@vellumai/assistant 0.10.4-dev.202607011113.fffc936 → 0.10.4-dev.202607011307.b79a0c5
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 +6 -0
- package/package.json +1 -1
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +84 -0
- package/src/daemon/lifecycle.ts +3 -6
- package/src/persistence/schema/conversation-groups.ts +12 -0
- package/src/persistence/schema/documents.ts +65 -0
- package/src/persistence/schema/index.ts +4 -0
- package/src/persistence/schema/memory-injection.ts +62 -0
- package/src/persistence/schema/workflows.ts +55 -0
- package/src/runtime/routes/conversation-routes.ts +32 -9
package/openapi.yaml
CHANGED
|
@@ -18137,6 +18137,12 @@ paths:
|
|
|
18137
18137
|
- low
|
|
18138
18138
|
- medium
|
|
18139
18139
|
- high
|
|
18140
|
+
hidden:
|
|
18141
|
+
description:
|
|
18142
|
+
When true, persist the user message but suppress it from the UI transcript (it stays in LLM-side history
|
|
18143
|
+
and still drives the turn). Used to prime a proactive assistant greeting without showing the
|
|
18144
|
+
triggering user message. Honored on the standard send path only.
|
|
18145
|
+
type: boolean
|
|
18140
18146
|
onboarding:
|
|
18141
18147
|
type: object
|
|
18142
18148
|
properties:
|
package/package.json
CHANGED
|
@@ -323,6 +323,90 @@ describe("handleSendMessage canonical guardian reply interception", () => {
|
|
|
323
323
|
expect(runAgentLoop).toHaveBeenCalledTimes(1);
|
|
324
324
|
});
|
|
325
325
|
|
|
326
|
+
test("hidden:true persists hidden metadata and still runs the turn", async () => {
|
|
327
|
+
listPendingByDestinationMock.mockReturnValue([]);
|
|
328
|
+
listCanonicalMock.mockReturnValue([]);
|
|
329
|
+
routeGuardianReplyMock.mockResolvedValue({
|
|
330
|
+
consumed: false,
|
|
331
|
+
decisionApplied: false,
|
|
332
|
+
type: "not_consumed",
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const persistUserMessage = mock(async () => ({
|
|
336
|
+
id: "persisted-user-id",
|
|
337
|
+
deduplicated: false,
|
|
338
|
+
}));
|
|
339
|
+
const runAgentLoop = mock(async () => undefined);
|
|
340
|
+
const session = {
|
|
341
|
+
setTrustContext: () => {},
|
|
342
|
+
updateClient: () => {},
|
|
343
|
+
emitConfirmationStateChanged: () => {},
|
|
344
|
+
emitActivityState: () => {},
|
|
345
|
+
setTurnChannelContext: () => {},
|
|
346
|
+
setTurnInterfaceContext: () => {},
|
|
347
|
+
ensureActorScopedHistory: async () => {},
|
|
348
|
+
usageStats: { inputTokens: 0, outputTokens: 0, estimatedCost: 0 },
|
|
349
|
+
isProcessing: () => false,
|
|
350
|
+
hasAnyPendingConfirmation: () => false,
|
|
351
|
+
denyAllPendingConfirmations: () => {},
|
|
352
|
+
enqueueMessage: () => ({ queued: true, requestId: "queued-id" }),
|
|
353
|
+
persistUserMessage,
|
|
354
|
+
runAgentLoop,
|
|
355
|
+
getMessages: () => [] as unknown[],
|
|
356
|
+
assistantId: "self",
|
|
357
|
+
trustContext: undefined,
|
|
358
|
+
hasPendingConfirmation: () => false,
|
|
359
|
+
setHostBrowserProxy: () => {},
|
|
360
|
+
setHostCuProxy: () => {},
|
|
361
|
+
setHostAppControlProxy: () => {},
|
|
362
|
+
restoreBrowserProxyAvailability: () => {},
|
|
363
|
+
addPreactivatedSkillId: () => {},
|
|
364
|
+
} as unknown as import("../daemon/conversation.js").Conversation;
|
|
365
|
+
|
|
366
|
+
const req = new Request("http://localhost/v1/messages", {
|
|
367
|
+
method: "POST",
|
|
368
|
+
headers: {
|
|
369
|
+
"Content-Type": "application/json",
|
|
370
|
+
"x-vellum-actor-principal-id": "test-user",
|
|
371
|
+
"x-vellum-principal-type": "actor",
|
|
372
|
+
},
|
|
373
|
+
body: JSON.stringify({
|
|
374
|
+
conversationKey: "guardian-conversation-key",
|
|
375
|
+
content: "Hey, how are you? Show me what you can do.",
|
|
376
|
+
sourceChannel: "vellum",
|
|
377
|
+
interface: "web",
|
|
378
|
+
hidden: true,
|
|
379
|
+
}),
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const res = await callHandler(
|
|
383
|
+
(args) =>
|
|
384
|
+
handleSendMessage(args, {
|
|
385
|
+
sendMessageDeps: {
|
|
386
|
+
getOrCreateConversation: async () => session,
|
|
387
|
+
assistantEventHub: { publish: async () => {} } as any,
|
|
388
|
+
resolveAttachments: () => [],
|
|
389
|
+
},
|
|
390
|
+
}),
|
|
391
|
+
req,
|
|
392
|
+
undefined,
|
|
393
|
+
202,
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
expect(res.status).toBe(202);
|
|
397
|
+
// The hidden message is persisted with metadata.hidden so the list-messages
|
|
398
|
+
// filter suppresses it from the UI transcript while keeping it in LLM-side
|
|
399
|
+
// history (see list-messages-hidden-metadata.test.ts for the filter).
|
|
400
|
+
expect(persistUserMessage).toHaveBeenCalledTimes(1);
|
|
401
|
+
const persistArgs = (persistUserMessage as any).mock.calls[0][0] as {
|
|
402
|
+
metadata?: { hidden?: boolean };
|
|
403
|
+
};
|
|
404
|
+
expect(persistArgs.metadata?.hidden).toBe(true);
|
|
405
|
+
// The turn still runs — the assistant's reply streams normally, so the chat
|
|
406
|
+
// reads as a proactive greeting.
|
|
407
|
+
expect(runAgentLoop).toHaveBeenCalledTimes(1);
|
|
408
|
+
});
|
|
409
|
+
|
|
326
410
|
test("excludes stale tool_approval hints without a live pending confirmation", async () => {
|
|
327
411
|
listPendingByDestinationMock.mockReturnValue([
|
|
328
412
|
{ id: "tool-approval-live", kind: "tool_approval" },
|
package/src/daemon/lifecycle.ts
CHANGED
|
@@ -873,12 +873,9 @@ export async function runDaemon(): Promise<void> {
|
|
|
873
873
|
writePid(process.pid);
|
|
874
874
|
|
|
875
875
|
// Install the `assistant` CLI symlink idempotently on every daemon start.
|
|
876
|
-
//
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
} catch (err) {
|
|
880
|
-
log.warn({ err }, "Assistant symlink installation failed — continuing");
|
|
881
|
-
}
|
|
876
|
+
// Best-effort and self-contained: every step swallows its own errors, so a
|
|
877
|
+
// failure never affects startup.
|
|
878
|
+
installAssistantSymlink();
|
|
882
879
|
|
|
883
880
|
startEmbeddingRuntimeManager();
|
|
884
881
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
2
|
+
|
|
3
|
+
export const conversationGroups = sqliteTable("conversation_groups", {
|
|
4
|
+
id: text("id").primaryKey(),
|
|
5
|
+
name: text("name").notNull(),
|
|
6
|
+
sortPosition: real("sort_position").notNull().default(0),
|
|
7
|
+
isSystemGroup: integer("is_system_group", { mode: "boolean" })
|
|
8
|
+
.notNull()
|
|
9
|
+
.default(false),
|
|
10
|
+
createdAt: integer("created_at").notNull(),
|
|
11
|
+
updatedAt: integer("updated_at").notNull(),
|
|
12
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
index,
|
|
3
|
+
integer,
|
|
4
|
+
primaryKey,
|
|
5
|
+
sqliteTable,
|
|
6
|
+
text,
|
|
7
|
+
} from "drizzle-orm/sqlite-core";
|
|
8
|
+
|
|
9
|
+
import { conversations } from "./conversations.js";
|
|
10
|
+
|
|
11
|
+
export const documents = sqliteTable("documents", {
|
|
12
|
+
surfaceId: text("surface_id").primaryKey(),
|
|
13
|
+
conversationId: text("conversation_id")
|
|
14
|
+
.notNull()
|
|
15
|
+
.references(() => conversations.id, { onDelete: "cascade" }),
|
|
16
|
+
title: text("title").notNull(),
|
|
17
|
+
content: text("content").notNull(),
|
|
18
|
+
wordCount: integer("word_count").notNull().default(0),
|
|
19
|
+
createdAt: integer("created_at").notNull(),
|
|
20
|
+
updatedAt: integer("updated_at").notNull(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Junction table mapping a document surface to every conversation it appears in.
|
|
24
|
+
export const documentConversations = sqliteTable(
|
|
25
|
+
"document_conversations",
|
|
26
|
+
{
|
|
27
|
+
surfaceId: text("surface_id")
|
|
28
|
+
.notNull()
|
|
29
|
+
.references(() => documents.surfaceId, { onDelete: "cascade" }),
|
|
30
|
+
conversationId: text("conversation_id").notNull(),
|
|
31
|
+
createdAt: integer("created_at").notNull(),
|
|
32
|
+
},
|
|
33
|
+
(table) => [
|
|
34
|
+
primaryKey({ columns: [table.surfaceId, table.conversationId] }),
|
|
35
|
+
index("idx_doc_conv_conversation_id").on(table.conversationId),
|
|
36
|
+
],
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
export const documentComments = sqliteTable(
|
|
40
|
+
"document_comments",
|
|
41
|
+
{
|
|
42
|
+
id: text("id").primaryKey(),
|
|
43
|
+
surfaceId: text("surface_id")
|
|
44
|
+
.notNull()
|
|
45
|
+
.references(() => documents.surfaceId, { onDelete: "cascade" }),
|
|
46
|
+
conversationId: text("conversation_id").notNull(),
|
|
47
|
+
author: text("author").notNull(),
|
|
48
|
+
content: text("content").notNull(),
|
|
49
|
+
anchorStart: integer("anchor_start"),
|
|
50
|
+
anchorEnd: integer("anchor_end"),
|
|
51
|
+
anchorText: text("anchor_text"),
|
|
52
|
+
// Self-referential parent link (threaded replies). The FK to
|
|
53
|
+
// document_comments(id) ON DELETE CASCADE is enforced at the DB level.
|
|
54
|
+
parentCommentId: text("parent_comment_id"),
|
|
55
|
+
status: text("status").notNull().default("open"),
|
|
56
|
+
resolvedBy: text("resolved_by"),
|
|
57
|
+
resolvedAt: integer("resolved_at"),
|
|
58
|
+
createdAt: integer("created_at").notNull(),
|
|
59
|
+
updatedAt: integer("updated_at").notNull(),
|
|
60
|
+
},
|
|
61
|
+
(table) => [
|
|
62
|
+
index("idx_document_comments_surface").on(table.surfaceId),
|
|
63
|
+
index("idx_document_comments_parent").on(table.parentCommentId),
|
|
64
|
+
],
|
|
65
|
+
);
|
|
@@ -3,12 +3,16 @@ export * from "./acp.js";
|
|
|
3
3
|
export * from "./bookmarks.js";
|
|
4
4
|
export * from "./calls.js";
|
|
5
5
|
export * from "./contacts.js";
|
|
6
|
+
export * from "./conversation-groups.js";
|
|
6
7
|
export * from "./conversations.js";
|
|
8
|
+
export * from "./documents.js";
|
|
7
9
|
export * from "./guardian.js";
|
|
8
10
|
export * from "./inference.js";
|
|
9
11
|
export * from "./infrastructure.js";
|
|
10
12
|
export * from "./memory-core.js";
|
|
11
13
|
export * from "./memory-graph.js";
|
|
14
|
+
export * from "./memory-injection.js";
|
|
12
15
|
export * from "./notifications.js";
|
|
13
16
|
export * from "./oauth.js";
|
|
14
17
|
export * from "./tasks.js";
|
|
18
|
+
export * from "./workflows.js";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
index,
|
|
3
|
+
integer,
|
|
4
|
+
primaryKey,
|
|
5
|
+
sqliteTable,
|
|
6
|
+
text,
|
|
7
|
+
} from "drizzle-orm/sqlite-core";
|
|
8
|
+
|
|
9
|
+
// Time-series of memory-v2 card injections, used by the router to decay a
|
|
10
|
+
// concept's recent injection pressure.
|
|
11
|
+
export const memoryV2InjectionEvents = sqliteTable(
|
|
12
|
+
"memory_v2_injection_events",
|
|
13
|
+
{
|
|
14
|
+
id: integer("id").primaryKey(),
|
|
15
|
+
slug: text("slug").notNull(),
|
|
16
|
+
injectedAt: integer("injected_at").notNull(),
|
|
17
|
+
},
|
|
18
|
+
(table) => [
|
|
19
|
+
index("idx_memory_v2_injection_events_slug_time").on(
|
|
20
|
+
table.slug,
|
|
21
|
+
table.injectedAt,
|
|
22
|
+
),
|
|
23
|
+
index("idx_memory_v2_injection_events_time").on(table.injectedAt),
|
|
24
|
+
],
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// Per-conversation record of every memory-v3 card ever injected, with a
|
|
28
|
+
// pruned_at tombstone so re-injection can be suppressed after pruning.
|
|
29
|
+
export const memoryV3EverInjected = sqliteTable(
|
|
30
|
+
"memory_v3_ever_injected",
|
|
31
|
+
{
|
|
32
|
+
conversationId: text("conversation_id").notNull(),
|
|
33
|
+
slug: text("slug").notNull(),
|
|
34
|
+
injectedAt: integer("injected_at").notNull(),
|
|
35
|
+
bytes: integer("bytes").notNull().default(0),
|
|
36
|
+
prunedAt: integer("pruned_at"),
|
|
37
|
+
},
|
|
38
|
+
(table) => [
|
|
39
|
+
primaryKey({ columns: [table.conversationId, table.slug] }),
|
|
40
|
+
index("idx_memory_v3_ever_injected_conv").on(table.conversationId),
|
|
41
|
+
],
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// Per-turn log of which memory-v3 cards were selected, with lane attribution.
|
|
45
|
+
export const memoryV3Selections = sqliteTable(
|
|
46
|
+
"memory_v3_selections",
|
|
47
|
+
{
|
|
48
|
+
conversationId: text("conversation_id").notNull(),
|
|
49
|
+
turn: integer("turn").notNull(),
|
|
50
|
+
slug: text("slug").notNull(),
|
|
51
|
+
source: text("source").notNull(),
|
|
52
|
+
pinned: integer("pinned").notNull().default(0),
|
|
53
|
+
createdAt: integer("created_at").notNull(),
|
|
54
|
+
messageId: text("message_id"),
|
|
55
|
+
sectionOrdinal: integer("section_ordinal"),
|
|
56
|
+
sectionTitle: text("section_title"),
|
|
57
|
+
},
|
|
58
|
+
(table) => [
|
|
59
|
+
primaryKey({ columns: [table.conversationId, table.turn, table.slug] }),
|
|
60
|
+
index("idx_memory_v3_selections_conv").on(table.conversationId, table.turn),
|
|
61
|
+
],
|
|
62
|
+
);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
index,
|
|
3
|
+
integer,
|
|
4
|
+
primaryKey,
|
|
5
|
+
sqliteTable,
|
|
6
|
+
text,
|
|
7
|
+
} from "drizzle-orm/sqlite-core";
|
|
8
|
+
|
|
9
|
+
export const workflowRuns = sqliteTable(
|
|
10
|
+
"workflow_runs",
|
|
11
|
+
{
|
|
12
|
+
id: text("id").primaryKey(),
|
|
13
|
+
name: text("name"),
|
|
14
|
+
scriptSource: text("script_source").notNull(),
|
|
15
|
+
scriptHash: text("script_hash").notNull(),
|
|
16
|
+
argsJson: text("args_json"),
|
|
17
|
+
capabilitiesJson: text("capabilities_json"),
|
|
18
|
+
status: text("status").notNull(),
|
|
19
|
+
conversationId: text("conversation_id"),
|
|
20
|
+
agentsSpawned: integer("agents_spawned").notNull().default(0),
|
|
21
|
+
inputTokens: integer("input_tokens").notNull().default(0),
|
|
22
|
+
outputTokens: integer("output_tokens").notNull().default(0),
|
|
23
|
+
resultJson: text("result_json"),
|
|
24
|
+
error: text("error"),
|
|
25
|
+
createdAt: integer("created_at"),
|
|
26
|
+
updatedAt: integer("updated_at"),
|
|
27
|
+
finishedAt: integer("finished_at"),
|
|
28
|
+
trustJson: text("trust_json"),
|
|
29
|
+
},
|
|
30
|
+
(table) => [
|
|
31
|
+
index("idx_workflow_runs_status_created_at").on(
|
|
32
|
+
table.status,
|
|
33
|
+
table.createdAt,
|
|
34
|
+
),
|
|
35
|
+
],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Append-only journal of each workflow's agent() calls, keyed by run + sequence
|
|
39
|
+
// for crash-recovery replay.
|
|
40
|
+
export const workflowJournal = sqliteTable(
|
|
41
|
+
"workflow_journal",
|
|
42
|
+
{
|
|
43
|
+
runId: text("run_id").notNull(),
|
|
44
|
+
seq: integer("seq").notNull(),
|
|
45
|
+
callHash: text("call_hash").notNull(),
|
|
46
|
+
kind: text("kind").notNull(),
|
|
47
|
+
requestJson: text("request_json"),
|
|
48
|
+
resultJson: text("result_json"),
|
|
49
|
+
status: text("status").notNull(),
|
|
50
|
+
createdAt: integer("created_at"),
|
|
51
|
+
inputTokens: integer("input_tokens"),
|
|
52
|
+
outputTokens: integer("output_tokens"),
|
|
53
|
+
},
|
|
54
|
+
(table) => [primaryKey({ columns: [table.runId, table.seq] })],
|
|
55
|
+
);
|
|
@@ -1275,6 +1275,11 @@ export async function handleSendMessage(
|
|
|
1275
1275
|
interface?: string;
|
|
1276
1276
|
conversationType?: string;
|
|
1277
1277
|
automated?: boolean;
|
|
1278
|
+
// Persist the user message but suppress it from the UI transcript (kept in
|
|
1279
|
+
// LLM history). Used by flows like research-onboarding's "Let's chat"
|
|
1280
|
+
// handoff to prime a proactive assistant greeting without showing the
|
|
1281
|
+
// triggering user message. Honored on the standard send path only.
|
|
1282
|
+
hidden?: boolean;
|
|
1278
1283
|
bypassSecretCheck?: boolean;
|
|
1279
1284
|
hostHomeDir?: string;
|
|
1280
1285
|
hostUsername?: string;
|
|
@@ -2356,7 +2361,13 @@ export async function handleSendMessage(
|
|
|
2356
2361
|
content: resolvedContent,
|
|
2357
2362
|
attachments,
|
|
2358
2363
|
requestId,
|
|
2359
|
-
metadata:
|
|
2364
|
+
metadata:
|
|
2365
|
+
body.automated === true || body.hidden === true
|
|
2366
|
+
? {
|
|
2367
|
+
...(body.automated === true ? { automated: true } : {}),
|
|
2368
|
+
...(body.hidden === true ? { hidden: true } : {}),
|
|
2369
|
+
}
|
|
2370
|
+
: undefined,
|
|
2360
2371
|
clientMessageId,
|
|
2361
2372
|
});
|
|
2362
2373
|
|
|
@@ -2370,14 +2381,20 @@ export async function handleSendMessage(
|
|
|
2370
2381
|
};
|
|
2371
2382
|
}
|
|
2372
2383
|
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2384
|
+
// A hidden message is suppressed from the UI transcript: don't echo it back
|
|
2385
|
+
// to clients (the echo would render a user bubble the list-messages filter
|
|
2386
|
+
// otherwise hides). The turn still runs below, and the assistant's reply
|
|
2387
|
+
// streams normally — so the chat reads as a proactive greeting.
|
|
2388
|
+
if (body.hidden !== true) {
|
|
2389
|
+
broadcastMessage({
|
|
2390
|
+
type: "user_message_echo",
|
|
2391
|
+
text: resolvedContent,
|
|
2392
|
+
conversationId: mapping.conversationId,
|
|
2393
|
+
messageId,
|
|
2394
|
+
requestId,
|
|
2395
|
+
clientMessageId,
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2381
2398
|
publishConversationMessagesChanged(mapping.conversationId, originClientId);
|
|
2382
2399
|
|
|
2383
2400
|
// Fire-and-forget the agent loop; events flow to the hub via broadcastMessage.
|
|
@@ -2890,6 +2907,12 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
2890
2907
|
.optional(),
|
|
2891
2908
|
inferenceProfile: z.string().nullable().optional(),
|
|
2892
2909
|
riskThreshold: z.enum(VALID_RISK_THRESHOLDS).optional(),
|
|
2910
|
+
hidden: z
|
|
2911
|
+
.boolean()
|
|
2912
|
+
.optional()
|
|
2913
|
+
.describe(
|
|
2914
|
+
"When true, persist the user message but suppress it from the UI transcript (it stays in LLM-side history and still drives the turn). Used to prime a proactive assistant greeting without showing the triggering user message. Honored on the standard send path only.",
|
|
2915
|
+
),
|
|
2893
2916
|
onboarding: z
|
|
2894
2917
|
.object({
|
|
2895
2918
|
tools: z.array(z.string()),
|