@vellumai/assistant 0.4.57 → 0.5.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/package.json +1 -1
- package/src/__tests__/assistant-feature-flags-integration.test.ts +7 -9
- package/src/__tests__/conversation-runtime-assembly.test.ts +28 -21
- package/src/__tests__/credential-execution-feature-gates.test.ts +3 -3
- package/src/__tests__/encrypted-store.test.ts +24 -12
- package/src/__tests__/file-read-tool.test.ts +40 -0
- package/src/__tests__/filesystem-tools.test.ts +4 -2
- package/src/__tests__/history-repair.test.ts +71 -0
- package/src/__tests__/host-file-read-tool.test.ts +87 -0
- package/src/__tests__/identity-intro-cache.test.ts +209 -0
- package/src/__tests__/model-intents.test.ts +1 -1
- package/src/__tests__/non-member-access-request.test.ts +3 -3
- package/src/__tests__/skill-feature-flags-integration.test.ts +18 -17
- package/src/__tests__/skill-feature-flags.test.ts +13 -13
- package/src/__tests__/skill-load-feature-flag.test.ts +4 -4
- package/src/__tests__/skill-memory.test.ts +14 -12
- package/src/__tests__/system-prompt.test.ts +8 -0
- package/src/config/feature-flag-registry.json +9 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +2 -39
- package/src/daemon/conversation-runtime-assembly.ts +4 -3
- package/src/daemon/history-repair.ts +28 -8
- package/src/daemon/trace-emitter.ts +3 -2
- package/src/memory/search/staleness.ts +4 -1
- package/src/notifications/decision-engine.ts +43 -2
- package/src/notifications/emit-signal.ts +1 -0
- package/src/permissions/checker.ts +0 -20
- package/src/prompts/system-prompt.ts +2 -0
- package/src/prompts/templates/BOOTSTRAP.md +10 -4
- package/src/prompts/templates/IDENTITY.md +1 -2
- package/src/providers/anthropic/client.ts +5 -17
- package/src/runtime/access-request-helper.ts +15 -1
- package/src/runtime/guardian-vellum-migration.ts +1 -3
- package/src/runtime/routes/btw-routes.ts +84 -0
- package/src/runtime/routes/identity-intro-cache.ts +105 -0
- package/src/runtime/routes/identity-routes.ts +51 -0
- package/src/runtime/routes/settings-routes.ts +1 -1
- package/src/security/encrypted-store.ts +1 -2
- package/src/skills/skill-memory.ts +5 -3
- package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
- package/src/telemetry/usage-telemetry-reporter.ts +2 -0
- package/src/tools/filesystem/read.ts +14 -3
- package/src/tools/host-filesystem/read.ts +17 -1
- package/src/tools/shared/filesystem/format-diff.ts +4 -16
- package/src/util/pricing.ts +4 -0
|
@@ -530,7 +530,6 @@ export class AnthropicProvider implements Provider {
|
|
|
530
530
|
private model: string;
|
|
531
531
|
private useNativeWebSearch: boolean;
|
|
532
532
|
private streamTimeoutMs: number;
|
|
533
|
-
private fastMode: boolean;
|
|
534
533
|
|
|
535
534
|
constructor(
|
|
536
535
|
apiKey: string,
|
|
@@ -542,9 +541,7 @@ export class AnthropicProvider implements Provider {
|
|
|
542
541
|
} = {},
|
|
543
542
|
) {
|
|
544
543
|
this.client = new Anthropic({ apiKey, baseURL: options.baseURL });
|
|
545
|
-
|
|
546
|
-
this.fastMode = model.endsWith("-fast");
|
|
547
|
-
this.model = this.fastMode ? model.slice(0, -"-fast".length) : model;
|
|
544
|
+
this.model = model;
|
|
548
545
|
this.useNativeWebSearch = options.useNativeWebSearch ?? false;
|
|
549
546
|
this.streamTimeoutMs = options.streamTimeoutMs ?? 300_000;
|
|
550
547
|
}
|
|
@@ -799,18 +796,9 @@ export class AnthropicProvider implements Provider {
|
|
|
799
796
|
|
|
800
797
|
let response: Anthropic.Message;
|
|
801
798
|
try {
|
|
802
|
-
const stream: UnifiedStream = this.
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
...params,
|
|
806
|
-
betas: ["fast-mode-2026-02-01"],
|
|
807
|
-
speed: "fast",
|
|
808
|
-
} as Parameters<typeof this.client.beta.messages.stream>[0],
|
|
809
|
-
{ signal: timeoutSignal },
|
|
810
|
-
) as unknown as UnifiedStream)
|
|
811
|
-
: (this.client.messages.stream(params, {
|
|
812
|
-
signal: timeoutSignal,
|
|
813
|
-
}) as unknown as UnifiedStream);
|
|
799
|
+
const stream: UnifiedStream = this.client.messages.stream(params, {
|
|
800
|
+
signal: timeoutSignal,
|
|
801
|
+
}) as unknown as UnifiedStream;
|
|
814
802
|
|
|
815
803
|
// Track whether we've seen a text content block so we can insert a
|
|
816
804
|
// separator between consecutive text blocks in the same response.
|
|
@@ -961,7 +949,7 @@ export class AnthropicProvider implements Provider {
|
|
|
961
949
|
content: response.content.map((block) =>
|
|
962
950
|
this.fromAnthropicBlock(block),
|
|
963
951
|
),
|
|
964
|
-
model:
|
|
952
|
+
model: response.model,
|
|
965
953
|
usage: {
|
|
966
954
|
inputTokens:
|
|
967
955
|
response.usage.input_tokens +
|
|
@@ -200,10 +200,24 @@ export function notifyGuardianOfAccessRequest(
|
|
|
200
200
|
});
|
|
201
201
|
|
|
202
202
|
let vellumDeliveryId: string | null = null;
|
|
203
|
+
// When the access request originates from a text channel with
|
|
204
|
+
// notification delivery support (Slack, Telegram), route the guardian
|
|
205
|
+
// notification to that same channel only. Delivering on the macOS
|
|
206
|
+
// client as well is noisy and approving from there doesn't work
|
|
207
|
+
// because the desktop path lacks the channel delivery context needed
|
|
208
|
+
// to deliver the verification code. Phone is excluded because it is
|
|
209
|
+
// not a deliverable notification channel.
|
|
210
|
+
const TEXT_CHANNELS_WITH_DELIVERY: ReadonlySet<string> = new Set([
|
|
211
|
+
"slack",
|
|
212
|
+
"telegram",
|
|
213
|
+
]);
|
|
214
|
+
const sameChannelOnly = TEXT_CHANNELS_WITH_DELIVERY.has(sourceChannel);
|
|
215
|
+
|
|
203
216
|
void emitNotificationSignal({
|
|
204
217
|
sourceEventName: "ingress.access_request",
|
|
205
218
|
sourceChannel: sourceChannel as NotificationSourceChannel,
|
|
206
219
|
sourceContextId: `access-req-${sourceChannel}-${actorExternalId}`,
|
|
220
|
+
...(sameChannelOnly ? { routingIntent: "single_channel" as const } : {}),
|
|
207
221
|
attentionHints: {
|
|
208
222
|
requiresAction: true,
|
|
209
223
|
urgency: "high",
|
|
@@ -258,7 +272,7 @@ export function notifyGuardianOfAccessRequest(
|
|
|
258
272
|
applyDeliveryStatus(delivery.id, result);
|
|
259
273
|
}
|
|
260
274
|
|
|
261
|
-
if (!vellumDeliveryId) {
|
|
275
|
+
if (!vellumDeliveryId && !sameChannelOnly) {
|
|
262
276
|
const fallback = createCanonicalGuardianDelivery({
|
|
263
277
|
requestId: canonicalRequest.id,
|
|
264
278
|
destinationChannel: "vellum",
|
|
@@ -91,9 +91,7 @@ export function ensureVellumGuardianBinding(
|
|
|
91
91
|
*
|
|
92
92
|
* Returns true if healing occurred, false otherwise.
|
|
93
93
|
*/
|
|
94
|
-
export function healGuardianBindingDrift(
|
|
95
|
-
incomingPrincipalId: string,
|
|
96
|
-
): boolean {
|
|
94
|
+
export function healGuardianBindingDrift(incomingPrincipalId: string): boolean {
|
|
97
95
|
if (!incomingPrincipalId.startsWith("vellum-principal-")) {
|
|
98
96
|
return false;
|
|
99
97
|
}
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* No messages are persisted. `conversation.processing` is never set or checked.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
16
|
+
|
|
15
17
|
import { buildToolDefinitions } from "../../daemon/conversation-tool-setup.js";
|
|
16
18
|
import { getConversationByKey } from "../../memory/conversation-key-store.js";
|
|
17
19
|
import { buildSystemPrompt } from "../../prompts/system-prompt.js";
|
|
@@ -21,13 +23,45 @@ import {
|
|
|
21
23
|
} from "../../providers/provider-send-message.js";
|
|
22
24
|
import { checkIngressForSecrets } from "../../security/secret-ingress.js";
|
|
23
25
|
import { getLogger } from "../../util/logger.js";
|
|
26
|
+
import { getWorkspacePromptPath } from "../../util/platform.js";
|
|
24
27
|
import type { AuthContext } from "../auth/types.js";
|
|
25
28
|
import { httpError } from "../http-errors.js";
|
|
26
29
|
import type { RouteDefinition } from "../http-router.js";
|
|
27
30
|
import type { SendMessageDeps } from "../http-types.js";
|
|
31
|
+
import { getCachedIntro, setCachedIntro } from "./identity-intro-cache.js";
|
|
28
32
|
|
|
29
33
|
const log = getLogger("btw-routes");
|
|
30
34
|
|
|
35
|
+
/** Conversation key used by the client for identity intro generation. */
|
|
36
|
+
const IDENTITY_INTRO_KEY = "identity-intro";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse the `## Identity Intro` section from SOUL.md.
|
|
40
|
+
* Returns the first non-empty line under that heading, or null.
|
|
41
|
+
*/
|
|
42
|
+
function readSoulIdentityIntro(): string | null {
|
|
43
|
+
try {
|
|
44
|
+
const soulPath = getWorkspacePromptPath("SOUL.md");
|
|
45
|
+
if (!existsSync(soulPath)) return null;
|
|
46
|
+
const content = readFileSync(soulPath, "utf-8");
|
|
47
|
+
|
|
48
|
+
let inSection = false;
|
|
49
|
+
for (const line of content.split("\n")) {
|
|
50
|
+
const trimmed = line.trim();
|
|
51
|
+
if (/^#+\s/.test(trimmed)) {
|
|
52
|
+
inSection = trimmed.toLowerCase().includes("identity intro");
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (inSection && trimmed.length > 0) {
|
|
56
|
+
return trimmed;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// Fall through — no SOUL.md intro available
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
31
65
|
// ---------------------------------------------------------------------------
|
|
32
66
|
// Handler
|
|
33
67
|
// ---------------------------------------------------------------------------
|
|
@@ -77,6 +111,43 @@ async function handleBtw(
|
|
|
77
111
|
);
|
|
78
112
|
}
|
|
79
113
|
|
|
114
|
+
// ----- Identity intro fast-path -----
|
|
115
|
+
// When the client requests the identity intro, check SOUL.md first (persisted
|
|
116
|
+
// during onboarding), then the LLM-generated cache. Only fall through to a
|
|
117
|
+
// live LLM call when neither source has a value.
|
|
118
|
+
if (conversationKey === IDENTITY_INTRO_KEY) {
|
|
119
|
+
const soulIntro = readSoulIdentityIntro();
|
|
120
|
+
const fastText = soulIntro ?? getCachedIntro()?.text;
|
|
121
|
+
if (fastText) {
|
|
122
|
+
log.debug(
|
|
123
|
+
soulIntro
|
|
124
|
+
? "Returning SOUL.md identity intro"
|
|
125
|
+
: "Returning cached identity intro",
|
|
126
|
+
);
|
|
127
|
+
const encoder = new TextEncoder();
|
|
128
|
+
const stream = new ReadableStream({
|
|
129
|
+
start(controller) {
|
|
130
|
+
controller.enqueue(
|
|
131
|
+
encoder.encode(
|
|
132
|
+
`event: btw_text_delta\ndata: ${JSON.stringify({ text: fastText })}\n\n`,
|
|
133
|
+
),
|
|
134
|
+
);
|
|
135
|
+
controller.enqueue(
|
|
136
|
+
encoder.encode(`event: btw_complete\ndata: {}\n\n`),
|
|
137
|
+
);
|
|
138
|
+
controller.close();
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
return new Response(stream, {
|
|
142
|
+
headers: {
|
|
143
|
+
"Content-Type": "text/event-stream",
|
|
144
|
+
"Cache-Control": "no-cache",
|
|
145
|
+
Connection: "keep-alive",
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
80
151
|
// Look up an existing conversation — never create one. BTW is ephemeral
|
|
81
152
|
// (the file header promises "No messages are persisted"), so we must not
|
|
82
153
|
// call getOrCreateConversation which would insert a DB row. When no
|
|
@@ -116,7 +187,9 @@ async function handleBtw(
|
|
|
116
187
|
? conversation.systemPrompt
|
|
117
188
|
: buildSystemPrompt({ excludeBootstrap: true });
|
|
118
189
|
|
|
190
|
+
const isIntroRequest = conversationKey === IDENTITY_INTRO_KEY;
|
|
119
191
|
let textDeltaCount = 0;
|
|
192
|
+
let collectedText = "";
|
|
120
193
|
await conversation.provider.sendMessage(
|
|
121
194
|
messages,
|
|
122
195
|
tools,
|
|
@@ -130,6 +203,7 @@ async function handleBtw(
|
|
|
130
203
|
onEvent: (event) => {
|
|
131
204
|
if (event.type === "text_delta") {
|
|
132
205
|
textDeltaCount++;
|
|
206
|
+
if (isIntroRequest) collectedText += event.text;
|
|
133
207
|
controller.enqueue(
|
|
134
208
|
encoder.encode(
|
|
135
209
|
`event: btw_text_delta\ndata: ${JSON.stringify({ text: event.text })}\n\n`,
|
|
@@ -148,6 +222,16 @@ async function handleBtw(
|
|
|
148
222
|
);
|
|
149
223
|
}
|
|
150
224
|
|
|
225
|
+
// Cache the generated identity intro for subsequent requests.
|
|
226
|
+
if (isIntroRequest && collectedText.trim()) {
|
|
227
|
+
try {
|
|
228
|
+
setCachedIntro(collectedText.trim());
|
|
229
|
+
log.debug("Cached identity intro text");
|
|
230
|
+
} catch {
|
|
231
|
+
// Non-fatal — next request will regenerate.
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
151
235
|
controller.enqueue(
|
|
152
236
|
encoder.encode(`event: btw_complete\ndata: {}\n\n`),
|
|
153
237
|
);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caching layer for the LLM-generated identity intro text.
|
|
3
|
+
*
|
|
4
|
+
* The intro (a short identity tagline) is generated via the
|
|
5
|
+
* /v1/btw endpoint and displayed on the Identity panel. To avoid redundant LLM
|
|
6
|
+
* calls, we cache the result for 4 hours with content-hash-based invalidation:
|
|
7
|
+
* when USER.md, IDENTITY.md, or SOUL.md change, the cache is busted.
|
|
8
|
+
*
|
|
9
|
+
* Storage uses the existing `memory_checkpoints` table (simple key-value store).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
getMemoryCheckpoint,
|
|
17
|
+
setMemoryCheckpoint,
|
|
18
|
+
} from "../../memory/checkpoints.js";
|
|
19
|
+
import { getWorkspacePromptPath } from "../../util/platform.js";
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Constants
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
const CACHE_TTL_MS = 4 * 60 * 60 * 1000; // 4 hours
|
|
26
|
+
|
|
27
|
+
const CHECKPOINT_KEY_TEXT = "identity:intro:text";
|
|
28
|
+
const CHECKPOINT_KEY_HASH = "identity:intro:content_hash";
|
|
29
|
+
const CHECKPOINT_KEY_TIMESTAMP = "identity:intro:cached_at";
|
|
30
|
+
|
|
31
|
+
/** Workspace files whose content influences the identity intro. */
|
|
32
|
+
const IDENTITY_FILES = ["USER.md", "IDENTITY.md", "SOUL.md"] as const;
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Helpers
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/** Read a workspace prompt file, returning empty string if missing. */
|
|
39
|
+
function readWorkspaceFile(name: string): string {
|
|
40
|
+
try {
|
|
41
|
+
const path = getWorkspacePromptPath(name);
|
|
42
|
+
if (!existsSync(path)) return "";
|
|
43
|
+
return readFileSync(path, "utf-8");
|
|
44
|
+
} catch {
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Compute a SHA-256 hex hash of the concatenated identity file contents. */
|
|
50
|
+
export function computeIdentityContentHash(): string {
|
|
51
|
+
const combined = IDENTITY_FILES.map(readWorkspaceFile).join("\n---\n");
|
|
52
|
+
return createHash("sha256").update(combined).digest("hex");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Public API
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
export interface CachedIntro {
|
|
60
|
+
text: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Retrieve the cached identity intro if it exists, is within the TTL window,
|
|
65
|
+
* and the identity files have not changed since it was generated.
|
|
66
|
+
*
|
|
67
|
+
* Returns `null` when the cache is missing, expired, or invalidated.
|
|
68
|
+
*/
|
|
69
|
+
export function getCachedIntro(): CachedIntro | null {
|
|
70
|
+
try {
|
|
71
|
+
const text = getMemoryCheckpoint(CHECKPOINT_KEY_TEXT);
|
|
72
|
+
const hash = getMemoryCheckpoint(CHECKPOINT_KEY_HASH);
|
|
73
|
+
const timestampStr = getMemoryCheckpoint(CHECKPOINT_KEY_TIMESTAMP);
|
|
74
|
+
|
|
75
|
+
if (!text || !hash || !timestampStr) return null;
|
|
76
|
+
|
|
77
|
+
// TTL check
|
|
78
|
+
const cachedAt = Number(timestampStr);
|
|
79
|
+
if (isNaN(cachedAt) || Date.now() - cachedAt > CACHE_TTL_MS) return null;
|
|
80
|
+
|
|
81
|
+
// Content-hash check — bust cache when identity files change
|
|
82
|
+
const currentHash = computeIdentityContentHash();
|
|
83
|
+
if (currentHash !== hash) return null;
|
|
84
|
+
|
|
85
|
+
return { text };
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Store the generated identity intro text in the cache along with
|
|
93
|
+
* the current content hash and timestamp.
|
|
94
|
+
*/
|
|
95
|
+
export function setCachedIntro(text: string): void {
|
|
96
|
+
try {
|
|
97
|
+
const hash = computeIdentityContentHash();
|
|
98
|
+
const now = String(Date.now());
|
|
99
|
+
setMemoryCheckpoint(CHECKPOINT_KEY_TEXT, text);
|
|
100
|
+
setMemoryCheckpoint(CHECKPOINT_KEY_HASH, hash);
|
|
101
|
+
setMemoryCheckpoint(CHECKPOINT_KEY_TIMESTAMP, now);
|
|
102
|
+
} catch {
|
|
103
|
+
// Cache write failure is non-fatal — next request will regenerate.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -11,6 +11,7 @@ import { getBaseDataDir } from "../../config/env-registry.js";
|
|
|
11
11
|
import { getWorkspacePromptPath, readLockfile } from "../../util/platform.js";
|
|
12
12
|
import { httpError } from "../http-errors.js";
|
|
13
13
|
import type { RouteDefinition } from "../http-router.js";
|
|
14
|
+
import { getCachedIntro } from "./identity-intro-cache.js";
|
|
14
15
|
|
|
15
16
|
interface DiskSpaceInfo {
|
|
16
17
|
path: string;
|
|
@@ -233,6 +234,51 @@ export function handleGetIdentity(): Response {
|
|
|
233
234
|
});
|
|
234
235
|
}
|
|
235
236
|
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// Identity intro cache
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Parse the `## Identity Intro` section from SOUL.md.
|
|
243
|
+
* Returns the first non-empty line under that heading, or null.
|
|
244
|
+
*/
|
|
245
|
+
function readSoulIdentityIntro(): string | null {
|
|
246
|
+
try {
|
|
247
|
+
const soulPath = getWorkspacePromptPath("SOUL.md");
|
|
248
|
+
if (!existsSync(soulPath)) return null;
|
|
249
|
+
const content = readFileSync(soulPath, "utf-8");
|
|
250
|
+
|
|
251
|
+
let inSection = false;
|
|
252
|
+
for (const line of content.split("\n")) {
|
|
253
|
+
const trimmed = line.trim();
|
|
254
|
+
if (/^#+\s/.test(trimmed)) {
|
|
255
|
+
inSection = trimmed.toLowerCase().includes("identity intro");
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (inSection && trimmed.length > 0) {
|
|
259
|
+
return trimmed;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
} catch {
|
|
263
|
+
// Fall through to cache/fallback
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function handleGetIdentityIntro(): Response {
|
|
269
|
+
// Prefer SOUL.md persisted intro over LLM-generated cache
|
|
270
|
+
const soulIntro = readSoulIdentityIntro();
|
|
271
|
+
if (soulIntro) {
|
|
272
|
+
return Response.json({ text: soulIntro });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const cached = getCachedIntro();
|
|
276
|
+
if (!cached) {
|
|
277
|
+
return httpError("NOT_FOUND", "No cached identity intro available", 404);
|
|
278
|
+
}
|
|
279
|
+
return Response.json({ text: cached.text });
|
|
280
|
+
}
|
|
281
|
+
|
|
236
282
|
// ---------------------------------------------------------------------------
|
|
237
283
|
// Route definitions
|
|
238
284
|
// ---------------------------------------------------------------------------
|
|
@@ -249,5 +295,10 @@ export function identityRouteDefinitions(): RouteDefinition[] {
|
|
|
249
295
|
method: "GET",
|
|
250
296
|
handler: () => handleGetIdentity(),
|
|
251
297
|
},
|
|
298
|
+
{
|
|
299
|
+
endpoint: "identity/intro",
|
|
300
|
+
method: "GET",
|
|
301
|
+
handler: () => handleGetIdentityIntro(),
|
|
302
|
+
},
|
|
252
303
|
];
|
|
253
304
|
}
|
|
@@ -104,8 +104,7 @@ export function _setStoreKeyPath(path: string | null): void {
|
|
|
104
104
|
|
|
105
105
|
function getStoreKeyPath(): string {
|
|
106
106
|
return (
|
|
107
|
-
storeKeyPathOverride ??
|
|
108
|
-
join(dirname(getStorePath()), STORE_KEY_FILENAME)
|
|
107
|
+
storeKeyPathOverride ?? join(dirname(getStorePath()), STORE_KEY_FILENAME)
|
|
109
108
|
);
|
|
110
109
|
}
|
|
111
110
|
|
|
@@ -17,8 +17,7 @@ const log = getLogger("skill-memory");
|
|
|
17
17
|
* Truncated to 500 chars max (matching the limit used by memory item extraction).
|
|
18
18
|
*/
|
|
19
19
|
export function buildCapabilityStatement(entry: CatalogSkill): string {
|
|
20
|
-
const displayName =
|
|
21
|
-
entry.metadata?.vellum?.["display-name"] ?? entry.name;
|
|
20
|
+
const displayName = entry.metadata?.vellum?.["display-name"] ?? entry.name;
|
|
22
21
|
const activationHints = entry.metadata?.vellum?.["activation-hints"];
|
|
23
22
|
|
|
24
23
|
let statement = `The "${displayName}" skill (${entry.id}) is available. ${entry.description}.`;
|
|
@@ -71,7 +70,10 @@ export function upsertSkillCapabilityMemory(
|
|
|
71
70
|
.get();
|
|
72
71
|
|
|
73
72
|
if (existing) {
|
|
74
|
-
if (
|
|
73
|
+
if (
|
|
74
|
+
existing.status === "active" &&
|
|
75
|
+
existing.fingerprint === fingerprint
|
|
76
|
+
) {
|
|
75
77
|
// Same content — just touch lastSeenAt
|
|
76
78
|
db.update(memoryItems)
|
|
77
79
|
.set({ lastSeenAt: now })
|
|
@@ -89,6 +89,10 @@ mock.module("../util/logger.js", () => ({
|
|
|
89
89
|
}),
|
|
90
90
|
}));
|
|
91
91
|
|
|
92
|
+
mock.module("../version.js", () => ({
|
|
93
|
+
APP_VERSION: "1.2.3-test",
|
|
94
|
+
}));
|
|
95
|
+
|
|
92
96
|
// ---------------------------------------------------------------------------
|
|
93
97
|
// Production import (after mocks)
|
|
94
98
|
// ---------------------------------------------------------------------------
|
|
@@ -370,8 +374,9 @@ describe("UsageTelemetryReporter", () => {
|
|
|
370
374
|
(mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
|
|
371
375
|
);
|
|
372
376
|
|
|
373
|
-
// Top-level: installation_id and events array (no turn_events key)
|
|
377
|
+
// Top-level: installation_id, app_version, and events array (no turn_events key)
|
|
374
378
|
expect(body.installation_id).toBe("test-device-id");
|
|
379
|
+
expect(body.app_version).toBe("1.2.3-test");
|
|
375
380
|
expect(Array.isArray(body.events)).toBe(true);
|
|
376
381
|
expect(body.events.length).toBe(1);
|
|
377
382
|
expect(body.turn_events).toBeUndefined();
|
|
@@ -26,6 +26,7 @@ import { resolveManagedProxyContext } from "../providers/managed-proxy/context.j
|
|
|
26
26
|
import { getExternalAssistantId } from "../runtime/auth/external-assistant-id.js";
|
|
27
27
|
import { getDeviceId } from "../util/device-id.js";
|
|
28
28
|
import { getLogger } from "../util/logger.js";
|
|
29
|
+
import { APP_VERSION } from "../version.js";
|
|
29
30
|
import type { TelemetryEvent } from "./types.js";
|
|
30
31
|
|
|
31
32
|
const log = getLogger("usage-telemetry");
|
|
@@ -192,6 +193,7 @@ export class UsageTelemetryReporter {
|
|
|
192
193
|
const payload = {
|
|
193
194
|
installation_id: getDeviceId(),
|
|
194
195
|
assistant_id: assistantId,
|
|
196
|
+
app_version: APP_VERSION,
|
|
195
197
|
...(organizationId ? { organization_id: organizationId } : {}),
|
|
196
198
|
...(userId ? { user_id: userId } : {}),
|
|
197
199
|
events: typedEvents,
|
|
@@ -61,7 +61,10 @@ class FileReadTool implements Tool {
|
|
|
61
61
|
if (IMAGE_EXTENSIONS.has(ext)) {
|
|
62
62
|
const pathCheck = sandboxPolicy(rawPath, context.workingDir);
|
|
63
63
|
if (!pathCheck.ok) {
|
|
64
|
-
return {
|
|
64
|
+
return {
|
|
65
|
+
content: `Error: ${pathCheck.error}. To read files outside the workspace, use the host_file_read tool instead.`,
|
|
66
|
+
isError: true,
|
|
67
|
+
};
|
|
65
68
|
}
|
|
66
69
|
return readImageFile(pathCheck.resolved);
|
|
67
70
|
}
|
|
@@ -89,8 +92,16 @@ class FileReadTool implements Tool {
|
|
|
89
92
|
content: `Error reading file "${rawPath}": ${error.message}`,
|
|
90
93
|
isError: true,
|
|
91
94
|
};
|
|
92
|
-
default:
|
|
93
|
-
|
|
95
|
+
default: {
|
|
96
|
+
const hint =
|
|
97
|
+
error.code === "PATH_OUT_OF_BOUNDS"
|
|
98
|
+
? ". To read files outside the workspace, use the host_file_read tool instead."
|
|
99
|
+
: "";
|
|
100
|
+
return {
|
|
101
|
+
content: `Error: ${error.message}${hint}`,
|
|
102
|
+
isError: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
94
105
|
}
|
|
95
106
|
}
|
|
96
107
|
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
+
import { extname } from "node:path";
|
|
2
|
+
|
|
1
3
|
import { RiskLevel } from "../../permissions/types.js";
|
|
2
4
|
import type { ToolDefinition } from "../../providers/types.js";
|
|
3
5
|
import { FileSystemOps } from "../shared/filesystem/file-ops-service.js";
|
|
6
|
+
import {
|
|
7
|
+
IMAGE_EXTENSIONS,
|
|
8
|
+
readImageFile,
|
|
9
|
+
} from "../shared/filesystem/image-read.js";
|
|
4
10
|
import { hostPolicy } from "../shared/filesystem/path-policy.js";
|
|
5
11
|
import type { Tool, ToolContext, ToolExecutionResult } from "../types.js";
|
|
6
12
|
|
|
7
13
|
class HostFileReadTool implements Tool {
|
|
8
14
|
name = "host_file_read";
|
|
9
15
|
description =
|
|
10
|
-
"Read the contents of a file on the host filesystem. Not for workspace files under .vellum (use file_read instead).";
|
|
16
|
+
"Read the contents of a file on the host filesystem, including images (JPEG, PNG, GIF, WebP). Not for workspace files under .vellum (use file_read instead).";
|
|
11
17
|
category = "host-filesystem";
|
|
12
18
|
defaultRiskLevel = RiskLevel.Medium;
|
|
13
19
|
|
|
@@ -63,6 +69,16 @@ class HostFileReadTool implements Tool {
|
|
|
63
69
|
);
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
// For image files, delegate to the shared image reader.
|
|
73
|
+
const ext = extname(rawPath).toLowerCase();
|
|
74
|
+
if (IMAGE_EXTENSIONS.has(ext)) {
|
|
75
|
+
const pathCheck = hostPolicy(rawPath);
|
|
76
|
+
if (!pathCheck.ok) {
|
|
77
|
+
return { content: `Error: ${pathCheck.error}`, isError: true };
|
|
78
|
+
}
|
|
79
|
+
return readImageFile(pathCheck.resolved);
|
|
80
|
+
}
|
|
81
|
+
|
|
66
82
|
const ops = new FileSystemOps(hostPolicy);
|
|
67
83
|
|
|
68
84
|
const result = ops.readFileSafe({
|
|
@@ -1,21 +1,15 @@
|
|
|
1
|
-
const MAX_DIFF_LINES = 8;
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
* Build
|
|
5
|
-
* Lines are prefixed with - /
|
|
2
|
+
* Build an inline diff from an old→new string replacement.
|
|
3
|
+
* Lines are prefixed with - / +.
|
|
6
4
|
*/
|
|
7
5
|
export function formatEditDiff(oldString: string, newString: string): string {
|
|
8
6
|
const removed =
|
|
9
7
|
oldString.length > 0
|
|
10
|
-
?
|
|
11
|
-
(l) => `- ${l}`,
|
|
12
|
-
)
|
|
8
|
+
? oldString.split("\n").map((l) => `- ${l}`)
|
|
13
9
|
: [];
|
|
14
10
|
const added =
|
|
15
11
|
newString.length > 0
|
|
16
|
-
?
|
|
17
|
-
(l) => `+ ${l}`,
|
|
18
|
-
)
|
|
12
|
+
? newString.split("\n").map((l) => `+ ${l}`)
|
|
19
13
|
: [];
|
|
20
14
|
|
|
21
15
|
return [...removed, ...added].join("\n");
|
|
@@ -37,9 +31,3 @@ export function formatWriteSummary(
|
|
|
37
31
|
return `(${oldLineCount} → ${newLineCount} lines)`;
|
|
38
32
|
}
|
|
39
33
|
|
|
40
|
-
function truncateLines(lines: string[], max: number): string[] {
|
|
41
|
-
if (lines.length <= max) return lines;
|
|
42
|
-
const kept = lines.slice(0, max);
|
|
43
|
-
kept.push(`... (${lines.length - max} more lines)`);
|
|
44
|
-
return kept;
|
|
45
|
-
}
|
package/src/util/pricing.ts
CHANGED
|
@@ -24,6 +24,9 @@ const PROVIDER_PRICING: Record<string, Record<string, ModelPricing>> = {
|
|
|
24
24
|
"claude-haiku-4": { inputPer1M: 0.8, outputPer1M: 4 },
|
|
25
25
|
},
|
|
26
26
|
openai: {
|
|
27
|
+
"gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15 },
|
|
28
|
+
"gpt-5.4-nano": { inputPer1M: 0.2, outputPer1M: 1.25 },
|
|
29
|
+
"gpt-5.2": { inputPer1M: 1.75, outputPer1M: 14 },
|
|
27
30
|
"gpt-4o": { inputPer1M: 2.5, outputPer1M: 10 },
|
|
28
31
|
"gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6 },
|
|
29
32
|
"gpt-4.1": { inputPer1M: 2.0, outputPer1M: 8.0 },
|
|
@@ -35,6 +38,7 @@ const PROVIDER_PRICING: Record<string, Record<string, ModelPricing>> = {
|
|
|
35
38
|
"o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4 },
|
|
36
39
|
},
|
|
37
40
|
gemini: {
|
|
41
|
+
"gemini-3-flash": { inputPer1M: 0.5, outputPer1M: 3 },
|
|
38
42
|
"gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10 },
|
|
39
43
|
"gemini-2.5-flash": { inputPer1M: 0.15, outputPer1M: 0.6 },
|
|
40
44
|
"gemini-2.0-flash": { inputPer1M: 0.1, outputPer1M: 0.4 },
|