@vellumai/assistant 0.10.5-staging.1 → 0.10.5-staging.2

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
@@ -6463,6 +6463,12 @@ paths:
6463
6463
  type: number
6464
6464
  inferenceProfile:
6465
6465
  type: string
6466
+ enabledPlugins:
6467
+ anyOf:
6468
+ - type: array
6469
+ items:
6470
+ type: string
6471
+ - type: "null"
6466
6472
  isProcessing:
6467
6473
  type: boolean
6468
6474
  required:
@@ -6802,6 +6808,12 @@ paths:
6802
6808
  type: number
6803
6809
  inferenceProfile:
6804
6810
  type: string
6811
+ enabledPlugins:
6812
+ anyOf:
6813
+ - type: array
6814
+ items:
6815
+ type: string
6816
+ - type: "null"
6805
6817
  isProcessing:
6806
6818
  type: boolean
6807
6819
  required:
@@ -7003,6 +7015,12 @@ paths:
7003
7015
  type: number
7004
7016
  inferenceProfile:
7005
7017
  type: string
7018
+ enabledPlugins:
7019
+ anyOf:
7020
+ - type: array
7021
+ items:
7022
+ type: string
7023
+ - type: "null"
7006
7024
  isProcessing:
7007
7025
  type: boolean
7008
7026
  required:
@@ -7210,6 +7228,56 @@ paths:
7210
7228
  description: Returned when the callId is missing or refers to a call in a different conversation.
7211
7229
  "404":
7212
7230
  description: Returned when the conversation or the referenced LLM call does not exist.
7231
+ /v1/conversations/{id}/enabledplugins:
7232
+ put:
7233
+ operationId: conversations_by_id_enabledplugins_put
7234
+ summary: Set conversation enabled plugins
7235
+ description:
7236
+ Scope a single conversation to a subset of installed plugins (first-party defaults are always available).
7237
+ Pass null to clear the scope back to the default (all enabled plugins).
7238
+ tags:
7239
+ - conversations
7240
+ parameters:
7241
+ - name: id
7242
+ in: path
7243
+ required: true
7244
+ schema:
7245
+ type: string
7246
+ requestBody:
7247
+ required: true
7248
+ content:
7249
+ application/json:
7250
+ schema:
7251
+ type: object
7252
+ properties:
7253
+ enabledPlugins:
7254
+ anyOf:
7255
+ - type: array
7256
+ items:
7257
+ type: string
7258
+ - type: "null"
7259
+ required:
7260
+ - enabledPlugins
7261
+ responses:
7262
+ "200":
7263
+ description: Successful response
7264
+ content:
7265
+ application/json:
7266
+ schema:
7267
+ type: object
7268
+ properties:
7269
+ conversationId:
7270
+ type: string
7271
+ enabledPlugins:
7272
+ anyOf:
7273
+ - type: array
7274
+ items:
7275
+ type: string
7276
+ - type: "null"
7277
+ required:
7278
+ - conversationId
7279
+ - enabledPlugins
7280
+ additionalProperties: false
7213
7281
  /v1/conversations/{id}/inference-profile:
7214
7282
  put:
7215
7283
  operationId: conversations_by_id_inferenceprofile_put
@@ -8140,6 +8208,12 @@ paths:
8140
8208
  type: number
8141
8209
  inferenceProfile:
8142
8210
  type: string
8211
+ enabledPlugins:
8212
+ anyOf:
8213
+ - type: array
8214
+ items:
8215
+ type: string
8216
+ - type: "null"
8143
8217
  isProcessing:
8144
8218
  type: boolean
8145
8219
  required:
@@ -17980,7 +18054,7 @@ paths:
17980
18054
  required:
17981
18055
  - acpSessionId
17982
18056
  additionalProperties: false
17983
- backgroundToolNotification:
18057
+ backgroundEventNotification:
17984
18058
  type: boolean
17985
18059
  backgroundToolCompletion:
17986
18060
  type: object
@@ -18216,9 +18290,12 @@ paths:
18216
18290
  - high
18217
18291
  hidden:
18218
18292
  description:
18219
- When true, persist the user message but suppress it from the UI transcript (it stays in LLM-side history
18220
- and still drives the turn). Used to prime a proactive assistant greeting without showing the
18221
- triggering user message. Honored on the standard send path only.
18293
+ "When true, persist the user message but suppress it from the UI transcript (it stays in LLM-side history
18294
+ and still drives the turn). Used for machine signals the user never typed (proactive-greeting
18295
+ priming, channel-setup wizard close). Suppression covers the queued path too: a hidden send that
18296
+ lands mid-turn returns { queued: true, requestId } but never appears in list-messages queued
18297
+ snapshots, emits no echo, and does not supersede pending interactions. Honored on the standard send
18298
+ path only — slash-command content bypasses it."
18222
18299
  type: boolean
18223
18300
  onboarding:
18224
18301
  type: object
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.10.5-staging.1",
3
+ "version": "0.10.5-staging.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -0,0 +1,184 @@
1
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import { makeMockLogger } from "./helpers/mock-logger.js";
4
+
5
+ mock.module("../util/logger.js", () => ({
6
+ getLogger: () => makeMockLogger(),
7
+ }));
8
+
9
+ const config = {
10
+ llm: {
11
+ profiles: {},
12
+ },
13
+ };
14
+
15
+ mock.module("../config/loader.js", () => ({
16
+ loadConfig: () => config,
17
+ getConfig: () => config,
18
+ }));
19
+
20
+ import {
21
+ createConversation,
22
+ getConversation,
23
+ setConversationEnabledPlugins,
24
+ } from "../persistence/conversation-crud.js";
25
+ import { getDb } from "../persistence/db-connection.js";
26
+ import { initializeDb } from "../persistence/db-init.js";
27
+ import { ROUTES } from "../runtime/routes/conversation-management-routes.js";
28
+ import { BadRequestError, NotFoundError } from "../runtime/routes/errors.js";
29
+ import { buildConversationDetailResponse } from "../runtime/services/conversation-serializer.js";
30
+ import { resetDbForTesting } from "./db-test-helpers.js";
31
+
32
+ await initializeDb();
33
+
34
+ const pluginsRoute = ROUTES.find(
35
+ (r) => r.operationId === "conversations_by_id_enabledplugins_put",
36
+ )!;
37
+
38
+ function clearTables(): void {
39
+ const db = getDb();
40
+ db.run("DELETE FROM conversation_assistant_attention_state");
41
+ db.run("DELETE FROM external_conversation_bindings");
42
+ db.run("DELETE FROM conversation_keys");
43
+ db.run("DELETE FROM messages");
44
+ db.run("DELETE FROM conversations");
45
+ }
46
+
47
+ describe("PUT /v1/conversations/:id/enabledplugins", () => {
48
+ beforeEach(() => {
49
+ clearTables();
50
+ });
51
+
52
+ afterAll(() => {
53
+ resetDbForTesting();
54
+ mock.restore();
55
+ });
56
+
57
+ test("persists a string[] scope and reflects it on the conversation", async () => {
58
+ const conversation = createConversation("enabledplugins-route");
59
+
60
+ const result = await pluginsRoute.handler({
61
+ pathParams: { id: conversation.id },
62
+ body: { enabledPlugins: ["plugin-a", "plugin-b"] },
63
+ headers: {},
64
+ });
65
+
66
+ expect(result).toMatchObject({
67
+ conversationId: conversation.id,
68
+ enabledPlugins: ["plugin-a", "plugin-b"],
69
+ });
70
+ expect(getConversation(conversation.id)?.enabledPlugins).toEqual([
71
+ "plugin-a",
72
+ "plugin-b",
73
+ ]);
74
+
75
+ // The HTTP conversation-detail response (what the web `conversationsByIdGet`
76
+ // reads) must also carry the scope, not just the internal DB getter.
77
+ const detail = buildConversationDetailResponse(conversation.id);
78
+ expect(detail?.conversation.enabledPlugins).toEqual([
79
+ "plugin-a",
80
+ "plugin-b",
81
+ ]);
82
+ });
83
+
84
+ test("detail response omits enabledPlugins by default and preserves an explicit []", async () => {
85
+ const conversation = createConversation("enabledplugins-detail-shape");
86
+
87
+ // Default (no per-chat restriction): the field is omitted from the wire.
88
+ expect(
89
+ "enabledPlugins" in buildConversationDetailResponse(conversation.id)!
90
+ .conversation,
91
+ ).toBe(false);
92
+
93
+ // Explicit empty scope (user cleared all optional plugins): preserved as [].
94
+ await pluginsRoute.handler({
95
+ pathParams: { id: conversation.id },
96
+ body: { enabledPlugins: [] },
97
+ headers: {},
98
+ });
99
+ expect(
100
+ buildConversationDetailResponse(conversation.id)?.conversation
101
+ .enabledPlugins,
102
+ ).toEqual([]);
103
+ });
104
+
105
+ test("clears the scope to default when enabledPlugins is null", async () => {
106
+ const conversation = createConversation("enabledplugins-clear");
107
+
108
+ await pluginsRoute.handler({
109
+ pathParams: { id: conversation.id },
110
+ body: { enabledPlugins: ["plugin-x"] },
111
+ headers: {},
112
+ });
113
+ expect(getConversation(conversation.id)?.enabledPlugins).toEqual([
114
+ "plugin-x",
115
+ ]);
116
+
117
+ const result = await pluginsRoute.handler({
118
+ pathParams: { id: conversation.id },
119
+ body: { enabledPlugins: null },
120
+ headers: {},
121
+ });
122
+
123
+ expect(result).toMatchObject({
124
+ conversationId: conversation.id,
125
+ enabledPlugins: null,
126
+ });
127
+ expect(getConversation(conversation.id)?.enabledPlugins).toBeNull();
128
+ });
129
+
130
+ test("rejects a non-array, non-null body with BadRequestError", async () => {
131
+ const conversation = createConversation("enabledplugins-bad");
132
+
133
+ await expect(
134
+ pluginsRoute.handler({
135
+ pathParams: { id: conversation.id },
136
+ body: { enabledPlugins: "not-an-array" },
137
+ headers: {},
138
+ }),
139
+ ).rejects.toThrow(BadRequestError);
140
+ expect(getConversation(conversation.id)?.enabledPlugins).toBeNull();
141
+ });
142
+
143
+ test("rejects an array containing a non-string with BadRequestError", async () => {
144
+ const conversation = createConversation("enabledplugins-bad-element");
145
+
146
+ await expect(
147
+ pluginsRoute.handler({
148
+ pathParams: { id: conversation.id },
149
+ body: { enabledPlugins: ["ok", 123] },
150
+ headers: {},
151
+ }),
152
+ ).rejects.toThrow(BadRequestError);
153
+ expect(getConversation(conversation.id)?.enabledPlugins).toBeNull();
154
+ });
155
+
156
+ test("rejects a missing enabledPlugins field without clearing the scope", async () => {
157
+ const conversation = createConversation("enabledplugins-missing");
158
+ // Seed an explicit scope, then send a body with no `enabledPlugins`.
159
+ setConversationEnabledPlugins(conversation.id, ["plugin-a"]);
160
+
161
+ await expect(
162
+ pluginsRoute.handler({
163
+ pathParams: { id: conversation.id },
164
+ body: {},
165
+ headers: {},
166
+ }),
167
+ ).rejects.toThrow(BadRequestError);
168
+ // The existing scope must survive a malformed request (null is the only
169
+ // explicit clear signal).
170
+ expect(getConversation(conversation.id)?.enabledPlugins).toEqual([
171
+ "plugin-a",
172
+ ]);
173
+ });
174
+
175
+ test("throws NotFoundError when the conversation does not exist", async () => {
176
+ await expect(
177
+ pluginsRoute.handler({
178
+ pathParams: { id: "missing" },
179
+ body: { enabledPlugins: [] },
180
+ headers: {},
181
+ }),
182
+ ).rejects.toThrow(NotFoundError);
183
+ });
184
+ });
@@ -3062,6 +3062,54 @@ describe("subagent notification user_message_echo suppression", () => {
3062
3062
  await new Promise((r) => setTimeout(r, 10));
3063
3063
  });
3064
3064
 
3065
+ test("drained hidden message persists with hidden metadata and emits no user_message_echo", async () => {
3066
+ const conversation = makeConversation();
3067
+ await conversation.loadFromDb();
3068
+
3069
+ const events1: ServerMessage[] = [];
3070
+ const eventsHidden: ServerMessage[] = [];
3071
+
3072
+ // Occupy the conversation so the hidden send queues — e.g. the user
3073
+ // closes the channel-setup wizard while the assistant is mid-turn.
3074
+ const p1 = conversation.processMessage({
3075
+ content: "msg-1",
3076
+ attachments: [],
3077
+ onEvent: (e) => events1.push(e),
3078
+ requestId: "req-1",
3079
+ });
3080
+ await waitForPendingRun(1);
3081
+
3082
+ // A hidden `POST /messages` send carries `hidden: true` metadata through
3083
+ // the queue branch (see conversation-routes.ts).
3084
+ conversation.enqueueMessage({
3085
+ content:
3086
+ "[User action on channel_setup surface: closed the slack setup wizard]",
3087
+ onEvent: (e) => eventsHidden.push(e),
3088
+ requestId: "req-hidden",
3089
+ metadata: { hidden: true },
3090
+ });
3091
+
3092
+ await resolveRun(0);
3093
+ await p1;
3094
+ await waitForPendingRun(2);
3095
+
3096
+ // Persisted with the hidden flag so the transcript filter keeps it out
3097
+ // of the rendered history...
3098
+ const persisted = capturedAddMessages.find((m) =>
3099
+ m.content.includes("channel_setup"),
3100
+ );
3101
+ expect(persisted?.metadata?.hidden).toBe(true);
3102
+ // ...the agent still wakes on it...
3103
+ expect(pendingRuns.length).toBe(2);
3104
+ // ...and no user_message_echo is broadcast, so no visible user bubble.
3105
+ expect(eventsHidden.some((e) => e.type === "user_message_echo")).toBe(
3106
+ false,
3107
+ );
3108
+
3109
+ await resolveRun(1);
3110
+ await new Promise((r) => setTimeout(r, 10));
3111
+ });
3112
+
3065
3113
  test("drained acp-notification message persists and wakes the agent but emits no user_message_echo", async () => {
3066
3114
  const conversation = makeConversation();
3067
3115
  await conversation.loadFromDb();