@vellumai/assistant 0.12.6 → 0.12.7-staging.1
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/.env.example +2 -0
- package/ARCHITECTURE.md +14 -14
- package/docs/architecture/integrations.md +5 -5
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +2 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +8 -4
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +2 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +8 -4
- package/node_modules/@vellumai/gateway-client/src/__tests__/route-trust-class.test.ts +111 -0
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +5 -2
- package/node_modules/@vellumai/gateway-client/src/index.ts +9 -0
- package/node_modules/@vellumai/gateway-client/src/route-trust-class.ts +94 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +2 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +8 -4
- package/openapi.yaml +153 -5
- package/package.json +1 -1
- package/src/__tests__/channel-policy.test.ts +7 -0
- package/src/__tests__/conversation-routes-disk-view.test.ts +141 -2
- package/src/__tests__/identity-routes.test.ts +1 -0
- package/src/__tests__/list-messages-client-message-id.test.ts +43 -0
- package/src/__tests__/openai-provider.test.ts +27 -0
- package/src/__tests__/skill-execute-input.test.ts +11 -0
- package/src/__tests__/tool-approval-handler.test.ts +4 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +26 -0
- package/src/agent/loop-unparseable-tool-retry.test.ts +333 -0
- package/src/agent/loop.ts +133 -12
- package/src/calls/__tests__/call-controller.test.ts +273 -1
- package/src/calls/__tests__/media-stream-server-integration.test.ts +40 -0
- package/src/calls/__tests__/media-stream-stt-session.test.ts +33 -0
- package/src/calls/call-constants.ts +10 -0
- package/src/calls/call-controller.ts +359 -25
- package/src/calls/media-stream-server.ts +18 -0
- package/src/calls/media-stream-stt-session.ts +30 -9
- package/src/channels/config.ts +8 -0
- package/src/cli/lib/bundled-marketplace.json +39 -0
- package/src/config/composer-preferences.test.ts +97 -0
- package/src/config/composer-preferences.ts +95 -0
- package/src/config/feature-flag-registry.json +16 -8
- package/src/config/schemas/llm.ts +1 -0
- package/src/contacts/types.ts +1 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +18 -2
- package/src/daemon/conversation-agent-loop.ts +3 -0
- package/src/daemon/conversation-messaging.ts +4 -0
- package/src/daemon/conversation-process.ts +15 -0
- package/src/daemon/conversation-queue-manager.ts +2 -0
- package/src/daemon/conversation.ts +2 -0
- package/src/daemon/handlers/shared.ts +13 -2
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/process-message.ts +5 -0
- package/src/i18n/__tests__/i18n.test.ts +25 -0
- package/src/i18n/assistant-text.ts +18 -0
- package/src/i18n/index.ts +1 -0
- package/src/i18n/messages.ts +11 -0
- package/src/ipc/routes/__tests__/route-adapter.test.ts +40 -0
- package/src/ipc/routes/route-adapter.ts +9 -0
- package/src/notifications/README.md +1 -1
- package/src/notifications/signal.ts +4 -0
- package/src/persistence/conversation-key-store.ts +12 -11
- package/src/providers/__tests__/unparseable-tool-args.test.ts +25 -0
- package/src/providers/inference/adapter-factory.ts +6 -0
- package/src/providers/ionet/client.test.ts +143 -0
- package/src/providers/ionet/client.ts +37 -0
- package/src/providers/model-catalog.ts +40 -0
- package/src/providers/openai/chat-completions-provider.ts +33 -2
- package/src/providers/retry.ts +10 -4
- package/src/providers/unparseable-tool-args.ts +29 -5
- package/src/runtime/__tests__/http-router-trust.test.ts +360 -0
- package/src/runtime/__tests__/shared-principal-lookup.test.ts +346 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +51 -1
- package/src/runtime/auth/__tests__/route-policy.test.ts +85 -1
- package/src/runtime/auth/__tests__/scopes.test.ts +36 -0
- package/src/runtime/auth/route-policy.ts +44 -0
- package/src/runtime/auth/scopes.ts +15 -0
- package/src/runtime/auth/types.ts +1 -0
- package/src/runtime/channel-reply-delivery.ts +2 -0
- package/src/runtime/http-router.ts +61 -1
- package/src/runtime/http-types.ts +3 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +35 -10
- package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +48 -0
- package/src/runtime/migrations/vbundle-builder.ts +19 -3
- package/src/runtime/migrations/vbundle-streaming-importer.ts +14 -19
- package/src/runtime/migrations/vbundle-streaming-validator.ts +14 -7
- package/src/runtime/migrations/vbundle-validator.ts +54 -1
- package/src/runtime/routes/composer-settings-routes.test.ts +71 -0
- package/src/runtime/routes/composer-settings-routes.ts +76 -0
- package/src/runtime/routes/conversation-routes.ts +54 -14
- package/src/runtime/routes/identity-routes.ts +2 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -0
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/migration-routes.ts +3 -1
- package/src/runtime/routes/types.ts +4 -1
- package/src/runtime/shared-principal-lookup.ts +235 -0
- package/src/tools/host-terminal/host-shell.ts +4 -2
- package/src/tools/shared/shell-output.test.ts +42 -0
- package/src/tools/shared/shell-output.ts +27 -4
- package/src/tools/skills/execute.ts +10 -7
- package/src/tools/terminal/shell.ts +2 -0
- package/src/tools/tool-approval-handler.ts +9 -1
package/src/channels/config.ts
CHANGED
|
@@ -99,6 +99,14 @@ const CHANNEL_POLICIES = {
|
|
|
99
99
|
conversationStrategy: "continue_existing_conversation",
|
|
100
100
|
},
|
|
101
101
|
},
|
|
102
|
+
"vellum-shared": {
|
|
103
|
+
notification: {
|
|
104
|
+
// A shared conversation has no transport: the contact's own client
|
|
105
|
+
// reads replies over its connection, so there is nowhere to push to.
|
|
106
|
+
deliveryEnabled: false,
|
|
107
|
+
conversationStrategy: "not_deliverable",
|
|
108
|
+
},
|
|
109
|
+
},
|
|
102
110
|
} as const satisfies Record<ChannelId, ChannelNotificationPolicy>;
|
|
103
111
|
|
|
104
112
|
export type ChannelPolicies = typeof CHANNEL_POLICIES;
|
|
@@ -148,6 +148,19 @@
|
|
|
148
148
|
"ref": "2d293cdcb68af03d446b61a9915a0ee3dc842d4e"
|
|
149
149
|
}
|
|
150
150
|
},
|
|
151
|
+
{
|
|
152
|
+
"name": "contractor-photo-to-quote",
|
|
153
|
+
"description": "Turns job-site and equipment photos into standardized trade estimates and quotes in Jobber or QuickBooks with multi-tier Good/Better/Best options in 5 minutes.",
|
|
154
|
+
"category": "commerce",
|
|
155
|
+
"icon": "📸",
|
|
156
|
+
"license": "MIT",
|
|
157
|
+
"homepage": "https://github.com/ZeebBoyBlue/contractor-photo-to-quote",
|
|
158
|
+
"source": {
|
|
159
|
+
"source": "github",
|
|
160
|
+
"repo": "ZeebBoyBlue/contractor-photo-to-quote",
|
|
161
|
+
"ref": "6c136186faaf03bcf125d202f0b1f51f97d94544"
|
|
162
|
+
}
|
|
163
|
+
},
|
|
151
164
|
{
|
|
152
165
|
"name": "contractor-voice-invoicer",
|
|
153
166
|
"description": "Converts 20-second truck voice memos and job photos into itemized client invoices with custom labor rates, material markups, and one-click accounting staging.",
|
|
@@ -474,6 +487,19 @@
|
|
|
474
487
|
"license": "MIT",
|
|
475
488
|
"icon": "\ud83d\udcac"
|
|
476
489
|
},
|
|
490
|
+
{
|
|
491
|
+
"name": "quickbooks-expense-categorizer",
|
|
492
|
+
"description": "Scans uncategorized QuickBooks Online expenses and bank feeds, matches receipts, and stages categorized entries with confidence scoring for 1-click batch review.",
|
|
493
|
+
"category": "commerce",
|
|
494
|
+
"icon": "🧾",
|
|
495
|
+
"license": "MIT",
|
|
496
|
+
"homepage": "https://github.com/ZeebBoyBlue/quickbooks-expense-categorizer",
|
|
497
|
+
"source": {
|
|
498
|
+
"source": "github",
|
|
499
|
+
"repo": "ZeebBoyBlue/quickbooks-expense-categorizer",
|
|
500
|
+
"ref": "120776619698cc3e9490caf5c756465f5caaa6c2"
|
|
501
|
+
}
|
|
502
|
+
},
|
|
477
503
|
{
|
|
478
504
|
"name": "reading-pal",
|
|
479
505
|
"source": {
|
|
@@ -500,6 +526,19 @@
|
|
|
500
526
|
"ref": "f1905e639941e98172c5032b6d722f668882d75d"
|
|
501
527
|
}
|
|
502
528
|
},
|
|
529
|
+
{
|
|
530
|
+
"name": "shopify-payout-reconciler",
|
|
531
|
+
"description": "Unbundles lump-sum Shopify merchant bank deposits into gross sales, processor fees, customer refunds, shipping labels, and sales tax for penny-accurate QuickBooks reconciliation.",
|
|
532
|
+
"category": "commerce",
|
|
533
|
+
"icon": "🛍️",
|
|
534
|
+
"license": "MIT",
|
|
535
|
+
"homepage": "https://github.com/ZeebBoyBlue/shopify-payout-reconciler",
|
|
536
|
+
"source": {
|
|
537
|
+
"source": "github",
|
|
538
|
+
"repo": "ZeebBoyBlue/shopify-payout-reconciler",
|
|
539
|
+
"ref": "079fe0d86629686243a588cf3c4867d38d6ab8b8"
|
|
540
|
+
}
|
|
541
|
+
},
|
|
503
542
|
{
|
|
504
543
|
"name": "simple-memory",
|
|
505
544
|
"source": {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mkdtempSync,
|
|
3
|
+
readdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
ComposerPreferencesPatchSchema,
|
|
14
|
+
readComposerPreferences,
|
|
15
|
+
updateComposerPreferences,
|
|
16
|
+
} from "./composer-preferences.js";
|
|
17
|
+
|
|
18
|
+
const directories: string[] = [];
|
|
19
|
+
function directory() {
|
|
20
|
+
const value = mkdtempSync(join(tmpdir(), "composer-preferences-"));
|
|
21
|
+
directories.push(value);
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
for (const path of directories.splice(0)) {
|
|
26
|
+
rmSync(path, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
describe("composer preferences", () => {
|
|
30
|
+
test("new users retain the assistant defaults", () => {
|
|
31
|
+
expect(readComposerPreferences("user-1", directory())).toEqual({
|
|
32
|
+
favoriteModeIds: [],
|
|
33
|
+
lastModeId: null,
|
|
34
|
+
lastAutonomy: null,
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
test("partial updates preserve unrelated choices and isolate users", () => {
|
|
38
|
+
const root = directory();
|
|
39
|
+
updateComposerPreferences(
|
|
40
|
+
"user-1",
|
|
41
|
+
{ lastModeId: "quality-optimized", lastAutonomy: "low" },
|
|
42
|
+
root,
|
|
43
|
+
);
|
|
44
|
+
const patch = ComposerPreferencesPatchSchema.safeParse({
|
|
45
|
+
favoriteModeIds: ["balanced"],
|
|
46
|
+
});
|
|
47
|
+
expect(patch.success).toBe(true);
|
|
48
|
+
if (!patch.success) {
|
|
49
|
+
throw patch.error;
|
|
50
|
+
}
|
|
51
|
+
const result = updateComposerPreferences("user-1", patch.data, root);
|
|
52
|
+
expect(result.lastModeId).toBe("quality-optimized");
|
|
53
|
+
expect(result.lastAutonomy).toBe("low");
|
|
54
|
+
expect(result.favoriteModeIds).toEqual(["balanced"]);
|
|
55
|
+
expect(readComposerPreferences("user-2", root).lastModeId).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
test("rejects invalid thresholds, excess favorites, and foreign fields", () => {
|
|
58
|
+
for (const value of [
|
|
59
|
+
{ lastAutonomy: "unrestricted" },
|
|
60
|
+
{ favoriteModeIds: ["1", "2", "3", "4", "5", "6"] },
|
|
61
|
+
{ principalId: "user-2" },
|
|
62
|
+
]) {
|
|
63
|
+
expect(ComposerPreferencesPatchSchema.safeParse(value).success).toBe(
|
|
64
|
+
false,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
test("deduplicates favorites and leaves no temporary files", () => {
|
|
69
|
+
const root = directory();
|
|
70
|
+
expect(
|
|
71
|
+
updateComposerPreferences(
|
|
72
|
+
"../../user-1",
|
|
73
|
+
{ favoriteModeIds: ["a", "a", "b"] },
|
|
74
|
+
root,
|
|
75
|
+
).favoriteModeIds,
|
|
76
|
+
).toEqual(["a", "b"]);
|
|
77
|
+
expect(readdirSync(join(root, "composer-preferences"))).toHaveLength(1);
|
|
78
|
+
});
|
|
79
|
+
test("does not overwrite corrupt preferences on a failed update", () => {
|
|
80
|
+
const root = directory();
|
|
81
|
+
updateComposerPreferences("user-1", {}, root);
|
|
82
|
+
const path = join(
|
|
83
|
+
root,
|
|
84
|
+
"composer-preferences",
|
|
85
|
+
readdirSync(join(root, "composer-preferences"))[0]!,
|
|
86
|
+
);
|
|
87
|
+
writeFileSync(path, '{"lastAutonomy":"invalid"}');
|
|
88
|
+
expect(() =>
|
|
89
|
+
updateComposerPreferences(
|
|
90
|
+
"user-1",
|
|
91
|
+
{ favoriteModeIds: ["balanced"] },
|
|
92
|
+
root,
|
|
93
|
+
),
|
|
94
|
+
).toThrow();
|
|
95
|
+
expect(readFileSync(path, "utf8")).toBe('{"lastAutonomy":"invalid"}');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
|
|
13
|
+
import { getDataDir } from "../util/platform.js";
|
|
14
|
+
|
|
15
|
+
export const ComposerPreferencesSchema = z.object({
|
|
16
|
+
favoriteModeIds: z.array(z.string().min(1).max(200)).max(5).default([]),
|
|
17
|
+
lastModeId: z.string().min(1).max(200).nullable().default(null),
|
|
18
|
+
lastAutonomy: z
|
|
19
|
+
.enum(["none", "low", "medium", "high"])
|
|
20
|
+
.nullable()
|
|
21
|
+
.default(null),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const ComposerPreferencesPatchSchema = z
|
|
25
|
+
.object({
|
|
26
|
+
favoriteModeIds:
|
|
27
|
+
ComposerPreferencesSchema.shape.favoriteModeIds.removeDefault(),
|
|
28
|
+
lastModeId: ComposerPreferencesSchema.shape.lastModeId.removeDefault(),
|
|
29
|
+
lastAutonomy: ComposerPreferencesSchema.shape.lastAutonomy.removeDefault(),
|
|
30
|
+
})
|
|
31
|
+
.partial()
|
|
32
|
+
.strict();
|
|
33
|
+
export type ComposerPreferences = z.infer<typeof ComposerPreferencesSchema>;
|
|
34
|
+
export type ComposerPreferencesPatch = z.infer<
|
|
35
|
+
typeof ComposerPreferencesPatchSchema
|
|
36
|
+
>;
|
|
37
|
+
|
|
38
|
+
function preferencesPath(principalId: string, dataDir: string): string {
|
|
39
|
+
const key = createHash("sha256").update(principalId).digest("hex");
|
|
40
|
+
return join(dataDir, "composer-preferences", `${key}.json`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function readComposerPreferences(
|
|
44
|
+
principalId: string,
|
|
45
|
+
dataDir = getDataDir(),
|
|
46
|
+
): ComposerPreferences {
|
|
47
|
+
let content: string;
|
|
48
|
+
try {
|
|
49
|
+
content = readFileSync(preferencesPath(principalId, dataDir), "utf8");
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
52
|
+
return ComposerPreferencesSchema.parse({});
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
const parsed = ComposerPreferencesSchema.safeParse(JSON.parse(content));
|
|
57
|
+
if (!parsed.success) {
|
|
58
|
+
throw new Error("Stored composer preferences are invalid", {
|
|
59
|
+
cause: parsed.error,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return parsed.data;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function updateComposerPreferences(
|
|
66
|
+
principalId: string,
|
|
67
|
+
patch: ComposerPreferencesPatch,
|
|
68
|
+
dataDir = getDataDir(),
|
|
69
|
+
): ComposerPreferences {
|
|
70
|
+
const parsed = ComposerPreferencesSchema.safeParse({
|
|
71
|
+
...readComposerPreferences(principalId, dataDir),
|
|
72
|
+
...patch,
|
|
73
|
+
});
|
|
74
|
+
if (!parsed.success) {
|
|
75
|
+
throw new Error("Invalid composer preferences", { cause: parsed.error });
|
|
76
|
+
}
|
|
77
|
+
const preferences = {
|
|
78
|
+
...parsed.data,
|
|
79
|
+
favoriteModeIds: [...new Set(parsed.data.favoriteModeIds)],
|
|
80
|
+
};
|
|
81
|
+
const directory = join(dataDir, "composer-preferences");
|
|
82
|
+
mkdirSync(directory, { recursive: true });
|
|
83
|
+
const path = preferencesPath(principalId, dataDir);
|
|
84
|
+
const temporaryPath = `${path}.${randomUUID()}.tmp`;
|
|
85
|
+
try {
|
|
86
|
+
writeFileSync(temporaryPath, JSON.stringify(preferences), {
|
|
87
|
+
mode: 0o600,
|
|
88
|
+
flag: "wx",
|
|
89
|
+
});
|
|
90
|
+
renameSync(temporaryPath, path);
|
|
91
|
+
} finally {
|
|
92
|
+
rmSync(temporaryPath, { force: true });
|
|
93
|
+
}
|
|
94
|
+
return preferences;
|
|
95
|
+
}
|
|
@@ -146,6 +146,14 @@
|
|
|
146
146
|
"description": "Auto-expand completed tool call step groups instead of showing them collapsed",
|
|
147
147
|
"defaultEnabled": false
|
|
148
148
|
},
|
|
149
|
+
{
|
|
150
|
+
"id": "quiet-turn-activity",
|
|
151
|
+
"scope": "client",
|
|
152
|
+
"key": "quiet-turn-activity",
|
|
153
|
+
"label": "Quiet Turn Activity",
|
|
154
|
+
"description": "While the assistant works, show a reply skeleton with one line naming where it is working instead of its intermediate text and steps; a settled turn shows only its opening line and its answer, with the steps behind a Worked summary in the message footer",
|
|
155
|
+
"defaultEnabled": false
|
|
156
|
+
},
|
|
149
157
|
{
|
|
150
158
|
"id": "inline-assistant-intermediates",
|
|
151
159
|
"scope": "client",
|
|
@@ -560,14 +568,6 @@
|
|
|
560
568
|
"description": "Streams an interactive virtual desktop from a platform-hosted, containerized assistant over /v1/desktop/stream. Requires platform hosting; self-hosted assistants are excluded. Off: the virtual desktop is hidden. On: the chat header offers its viewer and controls.",
|
|
561
569
|
"defaultEnabled": false
|
|
562
570
|
},
|
|
563
|
-
{
|
|
564
|
-
"id": "assistant-inbox",
|
|
565
|
-
"scope": "client",
|
|
566
|
-
"key": "assistant-inbox",
|
|
567
|
-
"label": "Assistant Inbox",
|
|
568
|
-
"description": "Shows an Assistant Inbox entry above Preferences in the web sidebar and the /assistant/inbox page it opens: a mailbox for the assistant's managed vellum.me address (received and sent, with a reading pane), the inline email setup card for entitled orgs with no address yet, and an upgrade card for orgs without the managed_email entitlement. With it on, email setup happens only there: the billing wizard hands off to the inbox instead of its own domain step, the billing page's finish-setup nudge gives way to the inbox's rail entry, and the Channels page's Email section points there instead of registering an address itself. Off hides the entry, redirects the route to chat, and keeps the older setup surfaces.",
|
|
569
|
-
"defaultEnabled": false
|
|
570
|
-
},
|
|
571
571
|
{
|
|
572
572
|
"id": "sidebar-done",
|
|
573
573
|
"scope": "both",
|
|
@@ -575,6 +575,14 @@
|
|
|
575
575
|
"label": "Sidebar Done",
|
|
576
576
|
"description": "Turns archiving a chat into marking it done: the sidebar row carries a Done check instead of the overflow menu, an All chats page lists the full history, and done chats stay findable in search by default. Off keeps today's Archive wording, the overflow menu, and search that hides archived rows unless the query asks for them.",
|
|
577
577
|
"defaultEnabled": false
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
"id": "vellum-trusted-contacts",
|
|
581
|
+
"scope": "both",
|
|
582
|
+
"key": "vellum-trusted-contacts",
|
|
583
|
+
"label": "Vellum Trusted Contacts",
|
|
584
|
+
"description": "Lets a guardian give another person access to talk to their assistant in the Vellum UI as a trusted contact. Gates contact-role token minting in the gateway, trusted-contact trust resolution for `vellum` channel principals, and the assistant's trusted-contact route allowlist. Off: none of those surfaces admit a contact, and the assistant stays reachable only by its guardian.",
|
|
585
|
+
"defaultEnabled": false
|
|
578
586
|
}
|
|
579
587
|
]
|
|
580
588
|
}
|
package/src/contacts/types.ts
CHANGED
|
@@ -107,6 +107,7 @@ import { faviconUrlForDomain } from "../util/favicon.js";
|
|
|
107
107
|
import { redactLogString } from "../util/log-redact.js";
|
|
108
108
|
import { getLogger } from "../util/logger.js";
|
|
109
109
|
import { withSqliteRetry } from "../util/sqlite-retry.js";
|
|
110
|
+
import { safeStringSlice } from "../util/unicode.js";
|
|
110
111
|
import type { DirectiveRequest } from "./assistant-attachments.js";
|
|
111
112
|
import {
|
|
112
113
|
cleanAssistantContent,
|
|
@@ -640,6 +641,8 @@ export interface EventHandlerState {
|
|
|
640
641
|
* rather than on every delta.
|
|
641
642
|
*/
|
|
642
643
|
readonly surfacePendingScannedToolUseIds: Set<string>;
|
|
644
|
+
/** App previews whose cumulative JSON reached the client payload cap. */
|
|
645
|
+
readonly cappedAppPreviewToolUseIds: Set<string>;
|
|
643
646
|
/**
|
|
644
647
|
* In-flight priming of {@link liveRevealGuardEntries}. The dispatcher
|
|
645
648
|
* awaits this before processing a `text_delta` (and before the
|
|
@@ -777,6 +780,7 @@ export function createEventHandlerState(): EventHandlerState {
|
|
|
777
780
|
forChatMintWatermark: currentForChatMintWatermark(),
|
|
778
781
|
stagedRevealIdentities: new Set(),
|
|
779
782
|
surfacePendingScannedToolUseIds: new Set(),
|
|
783
|
+
cappedAppPreviewToolUseIds: new Set(),
|
|
780
784
|
liveRevealGuardPriming: undefined,
|
|
781
785
|
};
|
|
782
786
|
}
|
|
@@ -1411,6 +1415,7 @@ function schedulePartialFlush(
|
|
|
1411
1415
|
// tools the client discards it (extractCodePreview only handles app tools),
|
|
1412
1416
|
// so we skip forwarding entirely to avoid transport/decode overhead.
|
|
1413
1417
|
const APP_TOOL_NAMES = new Set(["app_create"]);
|
|
1418
|
+
const APP_INPUT_PREVIEW_CHAR_LIMIT = 256_000;
|
|
1414
1419
|
|
|
1415
1420
|
// ── Surface Placeholder Detection ────────────────────────────────────
|
|
1416
1421
|
// A `visual` ui_show streams the longest tool input the model produces, and
|
|
@@ -2111,13 +2116,24 @@ export function handleInputJsonDelta(
|
|
|
2111
2116
|
// Only forward input deltas for app tools — the client only uses this
|
|
2112
2117
|
// stream for app_create code previews. Non-app tools would send large
|
|
2113
2118
|
// cumulative JSON on every delta with no benefit.
|
|
2114
|
-
if (
|
|
2119
|
+
if (
|
|
2120
|
+
!APP_TOOL_NAMES.has(event.toolName) ||
|
|
2121
|
+
state.cappedAppPreviewToolUseIds.has(event.toolUseId)
|
|
2122
|
+
) {
|
|
2115
2123
|
return;
|
|
2116
2124
|
}
|
|
2125
|
+
const content = safeStringSlice(
|
|
2126
|
+
event.accumulatedJson,
|
|
2127
|
+
0,
|
|
2128
|
+
APP_INPUT_PREVIEW_CHAR_LIMIT,
|
|
2129
|
+
);
|
|
2130
|
+
if (content.length < event.accumulatedJson.length) {
|
|
2131
|
+
state.cappedAppPreviewToolUseIds.add(event.toolUseId);
|
|
2132
|
+
}
|
|
2117
2133
|
deps.onEvent({
|
|
2118
2134
|
type: "tool_input_delta",
|
|
2119
2135
|
toolName: event.toolName,
|
|
2120
|
-
content
|
|
2136
|
+
content,
|
|
2121
2137
|
conversationId: deps.ctx.conversationId,
|
|
2122
2138
|
toolUseId: event.toolUseId,
|
|
2123
2139
|
messageId: state.lastAssistantMessageId,
|
|
@@ -44,6 +44,7 @@ import { isSendUserMessageActiveForTurn } from "../config/send-user-message-gate
|
|
|
44
44
|
import { desktopAutomationLease } from "../desktop/desktop-automation-lease.js";
|
|
45
45
|
import { writeRelationshipState } from "../home/relationship-state-writer.js";
|
|
46
46
|
import type { UserPromptSubmitInputContext } from "../hooks/types.js";
|
|
47
|
+
import type { SupportedLocale } from "../i18n/index.js";
|
|
47
48
|
import {
|
|
48
49
|
addMessage,
|
|
49
50
|
deleteMessageById,
|
|
@@ -500,6 +501,7 @@ export async function runAgentLoopImpl(
|
|
|
500
501
|
* loop defaults to `'mainAgent'` for user-initiated turns.
|
|
501
502
|
*/
|
|
502
503
|
callSite?: LLMCallSite;
|
|
504
|
+
locale?: SupportedLocale;
|
|
503
505
|
/** Skip fresh retrieval while keeping resident memory and static context. */
|
|
504
506
|
skipMemoryRetrieval?: boolean;
|
|
505
507
|
/**
|
|
@@ -1719,6 +1721,7 @@ export async function runAgentLoopImpl(
|
|
|
1719
1721
|
onCheckpoint,
|
|
1720
1722
|
callSite: turnCallSite,
|
|
1721
1723
|
inferenceCallSite,
|
|
1724
|
+
locale: options?.locale,
|
|
1722
1725
|
suppressAssistantText: sendUserMessageActive,
|
|
1723
1726
|
supportsDynamicUi: conversationSupportsDynamicUi(ctx),
|
|
1724
1727
|
trust: loopTrust,
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
parseInterfaceId,
|
|
27
27
|
} from "../channels/types.js";
|
|
28
28
|
import { parseImageDimensions } from "../context/image-dimensions.js";
|
|
29
|
+
import type { SupportedLocale } from "../i18n/index.js";
|
|
29
30
|
import {
|
|
30
31
|
type ProviderMessageMetadata,
|
|
31
32
|
providerMessageMetadataSchema,
|
|
@@ -824,6 +825,7 @@ export interface EnqueueMessageOptions {
|
|
|
824
825
|
currentPage?: string;
|
|
825
826
|
metadata?: Record<string, unknown>;
|
|
826
827
|
isInteractive?: boolean;
|
|
828
|
+
locale?: SupportedLocale;
|
|
827
829
|
displayContent?: string;
|
|
828
830
|
transport?: ConversationTransportMetadata;
|
|
829
831
|
clientMessageId?: string;
|
|
@@ -873,6 +875,7 @@ export function enqueueMessage(
|
|
|
873
875
|
currentPage,
|
|
874
876
|
metadata,
|
|
875
877
|
isInteractive,
|
|
878
|
+
locale,
|
|
876
879
|
displayContent,
|
|
877
880
|
transport,
|
|
878
881
|
clientMessageId,
|
|
@@ -912,6 +915,7 @@ export function enqueueMessage(
|
|
|
912
915
|
turnChannelContext,
|
|
913
916
|
turnInterfaceContext,
|
|
914
917
|
isInteractive,
|
|
918
|
+
locale,
|
|
915
919
|
sourceActorPrincipalId,
|
|
916
920
|
authContext: queuedAuthContext,
|
|
917
921
|
trustContext: queuedTrustContext,
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
type TurnInterfaceContext,
|
|
21
21
|
} from "../channels/types.js";
|
|
22
22
|
import type { LLMCallSite } from "../config/schemas/llm.js";
|
|
23
|
+
import type { SupportedLocale } from "../i18n/index.js";
|
|
23
24
|
import { extractPreferences } from "../notifications/preference-extractor.js";
|
|
24
25
|
import { createPreference } from "../notifications/preferences-store.js";
|
|
25
26
|
import {
|
|
@@ -1278,6 +1279,7 @@ async function drainSingleMessage(
|
|
|
1278
1279
|
titleText?: string;
|
|
1279
1280
|
isHiddenPrompt?: boolean;
|
|
1280
1281
|
turnTrustContext?: TrustContext;
|
|
1282
|
+
locale?: SupportedLocale;
|
|
1281
1283
|
cronRunId?: string | null;
|
|
1282
1284
|
} = {
|
|
1283
1285
|
isUserMessage: true,
|
|
@@ -1289,6 +1291,9 @@ async function drainSingleMessage(
|
|
|
1289
1291
|
if (next.isInteractive !== undefined) {
|
|
1290
1292
|
drainLoopOptions.isInteractive = next.isInteractive;
|
|
1291
1293
|
}
|
|
1294
|
+
if (next.locale !== undefined) {
|
|
1295
|
+
drainLoopOptions.locale = next.locale;
|
|
1296
|
+
}
|
|
1292
1297
|
if (agentLoopContent !== resolvedContent) {
|
|
1293
1298
|
drainLoopOptions.titleText = resolvedContent;
|
|
1294
1299
|
}
|
|
@@ -1797,6 +1802,7 @@ async function drainBatch(
|
|
|
1797
1802
|
isHiddenPrompt?: boolean;
|
|
1798
1803
|
notifyUserMessageId?: string;
|
|
1799
1804
|
turnTrustContext?: TrustContext;
|
|
1805
|
+
locale?: SupportedLocale;
|
|
1800
1806
|
cronRunId?: string | null;
|
|
1801
1807
|
} = {
|
|
1802
1808
|
isUserMessage: true,
|
|
@@ -1816,6 +1822,9 @@ async function drainBatch(
|
|
|
1816
1822
|
if (lastSuccessfulBatchEntry?.isInteractive !== undefined) {
|
|
1817
1823
|
drainLoopOptions.isInteractive = lastSuccessfulBatchEntry.isInteractive;
|
|
1818
1824
|
}
|
|
1825
|
+
if (lastSuccessfulBatchEntry?.locale !== undefined) {
|
|
1826
|
+
drainLoopOptions.locale = lastSuccessfulBatchEntry.locale;
|
|
1827
|
+
}
|
|
1819
1828
|
// A batch counts as a hidden turn only when every message in it is a
|
|
1820
1829
|
// hidden machine signal — one genuine user prompt justifies the
|
|
1821
1830
|
// prompt-as-user-speech consumers (title generation).
|
|
@@ -1871,6 +1880,7 @@ export interface ProcessMessageOptions {
|
|
|
1871
1880
|
currentPage?: string;
|
|
1872
1881
|
isInteractive?: boolean;
|
|
1873
1882
|
callSite?: LLMCallSite;
|
|
1883
|
+
locale?: SupportedLocale;
|
|
1874
1884
|
/**
|
|
1875
1885
|
* Optional ad-hoc inference-profile override applied to every LLM call
|
|
1876
1886
|
* this turn issues (e.g. a schedule's pinned profile). Forwarded to
|
|
@@ -1928,6 +1938,7 @@ export async function processMessage(
|
|
|
1928
1938
|
currentPage,
|
|
1929
1939
|
isInteractive,
|
|
1930
1940
|
callSite,
|
|
1941
|
+
locale,
|
|
1931
1942
|
overrideProfile,
|
|
1932
1943
|
displayContent,
|
|
1933
1944
|
sourceActorPrincipalId,
|
|
@@ -2451,6 +2462,7 @@ export async function processMessage(
|
|
|
2451
2462
|
isUserMessage?: boolean;
|
|
2452
2463
|
titleText?: string;
|
|
2453
2464
|
callSite?: LLMCallSite;
|
|
2465
|
+
locale?: SupportedLocale;
|
|
2454
2466
|
overrideProfile?: string;
|
|
2455
2467
|
turnTrustContext?: TrustContext;
|
|
2456
2468
|
} = {
|
|
@@ -2471,6 +2483,9 @@ export async function processMessage(
|
|
|
2471
2483
|
if (callSite !== undefined) {
|
|
2472
2484
|
loopOptions.callSite = callSite;
|
|
2473
2485
|
}
|
|
2486
|
+
if (locale !== undefined) {
|
|
2487
|
+
loopOptions.locale = locale;
|
|
2488
|
+
}
|
|
2474
2489
|
if (overrideProfile !== undefined) {
|
|
2475
2490
|
loopOptions.overrideProfile = overrideProfile;
|
|
2476
2491
|
}
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
TurnChannelContext,
|
|
11
11
|
TurnInterfaceContext,
|
|
12
12
|
} from "../channels/types.js";
|
|
13
|
+
import type { SupportedLocale } from "../i18n/index.js";
|
|
13
14
|
import type { AuthContext } from "../runtime/auth/types.js";
|
|
14
15
|
import { getLogger } from "../util/logger.js";
|
|
15
16
|
import type { UserMessageAttachment } from "./message-protocol.js";
|
|
@@ -30,6 +31,7 @@ export interface QueuedMessage {
|
|
|
30
31
|
turnInterfaceContext?: TurnInterfaceContext;
|
|
31
32
|
/** When false, the turn has no interactive user and should skip clarification prompts. */
|
|
32
33
|
isInteractive?: boolean;
|
|
34
|
+
locale?: SupportedLocale;
|
|
33
35
|
/** Requester identity captured from the verified auth context at enqueue time. */
|
|
34
36
|
sourceActorPrincipalId?: string;
|
|
35
37
|
/** Full auth snapshot captured at enqueue time for turn-scoped authorization decisions. */
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
derefToolResultReReads,
|
|
45
45
|
postTurnTruncateToolResults,
|
|
46
46
|
} from "../context/post-turn-tool-result-truncation.js";
|
|
47
|
+
import type { SupportedLocale } from "../i18n/index.js";
|
|
47
48
|
import { readProviderMetadata } from "../messaging/read-provider-metadata.js";
|
|
48
49
|
import { isGuardianCardRow } from "../notifications/approval-card-data.js";
|
|
49
50
|
import { PermissionPrompter } from "../permissions/prompter.js";
|
|
@@ -3528,6 +3529,7 @@ export class Conversation {
|
|
|
3528
3529
|
*/
|
|
3529
3530
|
replyDeliveredInAppOnly?: boolean;
|
|
3530
3531
|
callSite?: LLMCallSite;
|
|
3532
|
+
locale?: SupportedLocale;
|
|
3531
3533
|
/** Skip fresh retrieval while keeping resident memory and static context. */
|
|
3532
3534
|
skipMemoryRetrieval?: boolean;
|
|
3533
3535
|
/** Provider configuration source when distinct from turn semantics. */
|
|
@@ -19,6 +19,10 @@ import type {
|
|
|
19
19
|
import { ConfirmationDecisionSchema } from "../../api/responses/conversation-message.js";
|
|
20
20
|
import { getConfig } from "../../config/loader.js";
|
|
21
21
|
import type { LLMCallSite, Speed } from "../../config/schemas/llm.js";
|
|
22
|
+
import {
|
|
23
|
+
resolveStoredMessageText,
|
|
24
|
+
type SupportedLocale,
|
|
25
|
+
} from "../../i18n/index.js";
|
|
22
26
|
import { ipcCall as gatewayIpcCall } from "../../ipc/gateway-client.js";
|
|
23
27
|
import type { ProviderMessageMetadata } from "../../messaging/provider-message-metadata.js";
|
|
24
28
|
import type { SecretPromptResult } from "../../permissions/secret-prompt-types.js";
|
|
@@ -409,6 +413,7 @@ export function renderHistoryContent(
|
|
|
409
413
|
>,
|
|
410
414
|
messageId?: string,
|
|
411
415
|
metadata?: unknown,
|
|
416
|
+
locale?: SupportedLocale,
|
|
412
417
|
): RenderedHistoryContent {
|
|
413
418
|
// A row whose turn routed its reply through `send_user_message` carries
|
|
414
419
|
// private working notes, so every consumer of this render (web history,
|
|
@@ -575,8 +580,14 @@ export function renderHistoryContent(
|
|
|
575
580
|
const rawText =
|
|
576
581
|
typeof block._redactionVersion === "number" &&
|
|
577
582
|
block._redactionVersion >= SENTINEL_REDACTION_VERSION
|
|
578
|
-
?
|
|
579
|
-
|
|
583
|
+
? locale
|
|
584
|
+
? resolveStoredMessageText(block.text, locale)
|
|
585
|
+
: block.text
|
|
586
|
+
: neutralizeRedactedSentinels(
|
|
587
|
+
locale
|
|
588
|
+
? resolveStoredMessageText(block.text, locale)
|
|
589
|
+
: block.text,
|
|
590
|
+
);
|
|
580
591
|
const displayText = unwrapExternalContentForDisplay(rawText);
|
|
581
592
|
// Skip empty/whitespace-only text blocks. During streaming the client
|
|
582
593
|
// discards empty text deltas (guard !text.isEmpty), so including them
|
|
@@ -11,6 +11,7 @@ export const SYNC_TAGS = {
|
|
|
11
11
|
assistantAvatar: "assistant:self:avatar",
|
|
12
12
|
assistantIdentity: "assistant:self:identity",
|
|
13
13
|
assistantConfig: "assistant:self:config",
|
|
14
|
+
assistantComposerPreferences: "assistant:self:composerPreferences",
|
|
14
15
|
assistantSounds: "assistant:self:sounds",
|
|
15
16
|
assistantSchedules: "assistant:self:schedules",
|
|
16
17
|
assistantDesktop: "assistant:self:desktop",
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
parseChannelId,
|
|
20
20
|
parseInterfaceId,
|
|
21
21
|
} from "../channels/types.js";
|
|
22
|
+
import type { SupportedLocale } from "../i18n/index.js";
|
|
22
23
|
import { resolveAttachmentsForPersist } from "../persistence/attachments-store.js";
|
|
23
24
|
import {
|
|
24
25
|
addMessage,
|
|
@@ -88,6 +89,8 @@ type ProcessMessageOptions = ConversationCreateOptions & {
|
|
|
88
89
|
sourceChannel?: string;
|
|
89
90
|
/** Originating interface (e.g. "cli", "web"). Defaults to "web". */
|
|
90
91
|
sourceInterface?: string;
|
|
92
|
+
/** Locale for daemon-authored text emitted during this turn. */
|
|
93
|
+
locale?: SupportedLocale;
|
|
91
94
|
/**
|
|
92
95
|
* Origin tag of the turn (the conversation's `TitleOrigin`, e.g.
|
|
93
96
|
* "memory_consolidation"), threaded from `runBackgroundJob`. Propagated
|
|
@@ -780,6 +783,7 @@ export async function processMessage(
|
|
|
780
783
|
onEvent: emitEvent,
|
|
781
784
|
isInteractive: options?.isInteractive ?? false,
|
|
782
785
|
isUserMessage: true,
|
|
786
|
+
locale: options?.locale,
|
|
783
787
|
...(options?.callSite ? { callSite: options.callSite } : {}),
|
|
784
788
|
...(options?.overrideProfile
|
|
785
789
|
? { overrideProfile: options.overrideProfile }
|
|
@@ -854,6 +858,7 @@ export async function processMessageInBackground(
|
|
|
854
858
|
onEvent: emitEvent,
|
|
855
859
|
isInteractive: options?.isInteractive ?? false,
|
|
856
860
|
isUserMessage: true,
|
|
861
|
+
locale: options?.locale,
|
|
857
862
|
...(options?.callSite ? { callSite: options.callSite } : {}),
|
|
858
863
|
...(options?.overrideProfile
|
|
859
864
|
? { overrideProfile: options.overrideProfile }
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
MESSAGE_KEYS,
|
|
9
9
|
negotiateLocale,
|
|
10
10
|
resolveConversationTitle,
|
|
11
|
+
resolveStoredMessageText,
|
|
11
12
|
SUPPORTED_LOCALES,
|
|
12
13
|
t,
|
|
13
14
|
} from "../index.js";
|
|
@@ -103,6 +104,30 @@ describe("resolveConversationTitle", () => {
|
|
|
103
104
|
});
|
|
104
105
|
});
|
|
105
106
|
|
|
107
|
+
describe("resolveStoredMessageText", () => {
|
|
108
|
+
test("resolves exact stored keys while preserving surrounding whitespace", () => {
|
|
109
|
+
expect(
|
|
110
|
+
resolveStoredMessageText(
|
|
111
|
+
`\n\n${MESSAGE_KEYS.AGENT_LOOP_UNPARSEABLE_TOOL_RETRY_STOP}`,
|
|
112
|
+
"es",
|
|
113
|
+
),
|
|
114
|
+
).toBe(
|
|
115
|
+
`\n\n${
|
|
116
|
+
MESSAGE_CATALOGS.es[MESSAGE_KEYS.AGENT_LOOP_UNPARSEABLE_TOOL_RETRY_STOP]
|
|
117
|
+
}`,
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("passes legacy and ordinary display text through", () => {
|
|
122
|
+
const legacy =
|
|
123
|
+
"I stopped after repeated malformed tool calls to keep this conversation from growing indefinitely. Please try again, or switch models if the problem continues.";
|
|
124
|
+
expect(resolveStoredMessageText(legacy, "es")).toBe(legacy);
|
|
125
|
+
expect(resolveStoredMessageText("ordinary assistant text", "zh")).toBe(
|
|
126
|
+
"ordinary assistant text",
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
106
131
|
describe("isMessageKey", () => {
|
|
107
132
|
test("recognizes catalog keys only", () => {
|
|
108
133
|
expect(isMessageKey(MESSAGE_KEYS.CONVERSATION_TITLE_UNTITLED)).toBe(true);
|