@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 +81 -4
- package/package.json +1 -1
- package/src/__tests__/conversation-enabledplugins-route.test.ts +184 -0
- package/src/__tests__/conversation-queue.test.ts +48 -0
- package/src/__tests__/conversation-routes-hidden-queue.test.ts +407 -0
- package/src/__tests__/list-messages-background-tool-completion.test.ts +6 -6
- package/src/__tests__/list-messages-queued.test.ts +25 -0
- package/src/__tests__/slack-app-setup-skill-regression.test.ts +24 -0
- package/src/__tests__/title-generate-hook.test.ts +16 -0
- package/src/api/responses/conversation-message.ts +6 -6
- package/src/daemon/conversation-agent-loop.ts +8 -0
- package/src/daemon/conversation-process.ts +40 -14
- package/src/daemon/conversation.ts +2 -0
- package/src/daemon/wake-conversation-ops.ts +7 -8
- package/src/persistence/conversation-crud.ts +26 -1
- package/src/plugin-api/types.ts +7 -0
- package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +5 -0
- package/src/runtime/routes/conversation-list-routes.ts +6 -0
- package/src/runtime/routes/conversation-management-routes.ts +71 -0
- package/src/runtime/routes/conversation-routes.ts +97 -65
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +13 -8
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +24 -9
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +19 -0
- package/src/runtime/services/conversation-serializer.ts +5 -0
- package/src/runtime/sync/resource-sync-events.ts +10 -0
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
|
-
|
|
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
|
|
18221
|
-
|
|
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
|
@@ -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();
|