@vellumai/assistant 0.12.2-staging.4 → 0.12.2-staging.5
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/config/bundled-skills/skill-management/TOOLS.json +1 -1
- package/src/live-voice/__tests__/live-voice-look-follow-up.test.ts +374 -0
- package/src/live-voice/__tests__/live-voice-vad.test.ts +34 -0
- package/src/live-voice/__tests__/protocol.test.ts +48 -0
- package/src/live-voice/__tests__/session-controls.test.ts +25 -0
- package/src/live-voice/live-voice-session.ts +210 -12
- package/src/live-voice/protocol.ts +20 -0
- package/src/live-voice/session-controls.ts +55 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +10 -0
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
- package/src/skills/managed-store.ts +56 -0
- package/src/tools/skills/find-similar-skills.test.ts +211 -2
- package/src/tools/skills/find-similar-skills.ts +62 -9
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
* `readInstallMeta` seam is mocked to key off the skill id (its dir basename).
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
17
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { basename, join } from "node:path";
|
|
19
|
+
import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
19
20
|
|
|
20
21
|
import type { SkillSource } from "../../config/skills.js";
|
|
22
|
+
import type { StoredManagedSkill } from "../../skills/managed-store.js";
|
|
21
23
|
import type { OwnerInfo } from "../types.js";
|
|
22
24
|
|
|
23
25
|
// Map managed skill id → recorded author, consulted by the mocked
|
|
@@ -44,6 +46,11 @@ function makeContext(enabledPluginSet?: Set<string> | null): ToolContext {
|
|
|
44
46
|
};
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
/** A retrospective-pass tool context: the one caller handed `current`. */
|
|
50
|
+
function makeRetrospectiveContext(): ToolContext {
|
|
51
|
+
return { ...makeContext(), requestOrigin: "memory_retrospective" };
|
|
52
|
+
}
|
|
53
|
+
|
|
47
54
|
const catalog = (
|
|
48
55
|
...skills: {
|
|
49
56
|
id: string;
|
|
@@ -217,6 +224,208 @@ describe("find_similar_skills — enrichment", () => {
|
|
|
217
224
|
});
|
|
218
225
|
});
|
|
219
226
|
|
|
227
|
+
describe("find_similar_skills: current skill for a refinable hit", () => {
|
|
228
|
+
afterEach(() => {
|
|
229
|
+
rmSync(join(process.env.VELLUM_WORKSPACE_DIR!, "skills"), {
|
|
230
|
+
recursive: true,
|
|
231
|
+
force: true,
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const refinableCatalog = () =>
|
|
236
|
+
catalog(
|
|
237
|
+
{
|
|
238
|
+
id: "weekly-export",
|
|
239
|
+
name: "Weekly Report Export",
|
|
240
|
+
description: "Export the weekly usage report",
|
|
241
|
+
source: "managed",
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
id: "user-skill",
|
|
245
|
+
name: "User Skill",
|
|
246
|
+
description: "A person wrote this",
|
|
247
|
+
source: "managed",
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
id: "clean-disk",
|
|
251
|
+
name: "Clean Disk",
|
|
252
|
+
description: "Free up disk space",
|
|
253
|
+
source: "bundled",
|
|
254
|
+
},
|
|
255
|
+
);
|
|
256
|
+
const stored: Record<string, StoredManagedSkill> = {
|
|
257
|
+
"weekly-export": {
|
|
258
|
+
name: "Weekly Report Export",
|
|
259
|
+
description: "Export the weekly usage report",
|
|
260
|
+
emoji: "📊",
|
|
261
|
+
category: "productivity",
|
|
262
|
+
includes: ["csv-basics"],
|
|
263
|
+
activationHints: ["user asks for the weekly report"],
|
|
264
|
+
avoidWhen: ["the report is monthly"],
|
|
265
|
+
body: "1. Open the dashboard.\n2. Export.",
|
|
266
|
+
},
|
|
267
|
+
"user-skill": {
|
|
268
|
+
name: "User Skill",
|
|
269
|
+
description: "A person wrote this",
|
|
270
|
+
body: "Do the user's thing.",
|
|
271
|
+
},
|
|
272
|
+
"clean-disk": {
|
|
273
|
+
name: "Clean Disk",
|
|
274
|
+
description: "Free up disk space",
|
|
275
|
+
body: "Delete caches.",
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
const reads: string[] = [];
|
|
279
|
+
const readStoredManagedSkill = (skillId: string) => {
|
|
280
|
+
reads.push(skillId);
|
|
281
|
+
return stored[skillId] ?? null;
|
|
282
|
+
};
|
|
283
|
+
const hits = async () => [
|
|
284
|
+
{ skillId: "weekly-export", score: 0.9 },
|
|
285
|
+
{ skillId: "user-skill", score: 0.8 },
|
|
286
|
+
{ skillId: "clean-disk", score: 0.7 },
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
test("the retrospective gets the skill as it is, in scaffold argument names, only for its own managed hits", async () => {
|
|
290
|
+
installMetaAuthors["weekly-export"] = "assistant";
|
|
291
|
+
installMetaAuthors["user-skill"] = "user";
|
|
292
|
+
|
|
293
|
+
const result = await executeFindSimilarSkills(
|
|
294
|
+
{ goal: "export the weekly report" },
|
|
295
|
+
makeRetrospectiveContext(),
|
|
296
|
+
{
|
|
297
|
+
nearestExistingSkills: hits,
|
|
298
|
+
loadCatalog: refinableCatalog,
|
|
299
|
+
readStoredManagedSkill,
|
|
300
|
+
},
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const { skills } = JSON.parse(result.content);
|
|
304
|
+
expect(skills[0].current).toEqual({
|
|
305
|
+
name: "Weekly Report Export",
|
|
306
|
+
description: "Export the weekly usage report",
|
|
307
|
+
emoji: "📊",
|
|
308
|
+
category: "productivity",
|
|
309
|
+
includes: ["csv-basics"],
|
|
310
|
+
activation_hints: ["user asks for the weekly report"],
|
|
311
|
+
avoid_when: ["the report is monthly"],
|
|
312
|
+
body_markdown: "1. Open the dashboard.\n2. Export.",
|
|
313
|
+
});
|
|
314
|
+
// A person's skill and a bundled skill are not the pass's to refine, so
|
|
315
|
+
// their content is not handed over.
|
|
316
|
+
expect(skills[1]).not.toHaveProperty("current");
|
|
317
|
+
expect(skills[2]).not.toHaveProperty("current");
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("absent frontmatter fields are omitted rather than sent as null", async () => {
|
|
321
|
+
installMetaAuthors["user-skill"] = "assistant";
|
|
322
|
+
|
|
323
|
+
const result = await executeFindSimilarSkills(
|
|
324
|
+
{ goal: "the user's thing" },
|
|
325
|
+
makeRetrospectiveContext(),
|
|
326
|
+
{
|
|
327
|
+
nearestExistingSkills: async () => [
|
|
328
|
+
{ skillId: "user-skill", score: 1 },
|
|
329
|
+
],
|
|
330
|
+
loadCatalog: refinableCatalog,
|
|
331
|
+
readStoredManagedSkill,
|
|
332
|
+
},
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
const { skills } = JSON.parse(result.content);
|
|
336
|
+
expect(skills[0].current).toEqual({
|
|
337
|
+
name: "User Skill",
|
|
338
|
+
description: "A person wrote this",
|
|
339
|
+
body_markdown: "Do the user's thing.",
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("an interactive caller never receives skill bodies, and no body is read", async () => {
|
|
344
|
+
installMetaAuthors["weekly-export"] = "assistant";
|
|
345
|
+
reads.length = 0;
|
|
346
|
+
|
|
347
|
+
const result = await executeFindSimilarSkills(
|
|
348
|
+
{ goal: "export the weekly report" },
|
|
349
|
+
makeContext(),
|
|
350
|
+
{
|
|
351
|
+
nearestExistingSkills: hits,
|
|
352
|
+
loadCatalog: refinableCatalog,
|
|
353
|
+
readStoredManagedSkill,
|
|
354
|
+
},
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
const { skills } = JSON.parse(result.content);
|
|
358
|
+
for (const skill of skills) {
|
|
359
|
+
expect(skill).not.toHaveProperty("current");
|
|
360
|
+
}
|
|
361
|
+
expect(reads).toEqual([]);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("the stored body keeps its leading indentation and drops only the store's separator newlines", async () => {
|
|
365
|
+
// The real reader, against a real SKILL.md: a body opening with an
|
|
366
|
+
// indented code block must come back indented, or a refinement that
|
|
367
|
+
// copies it turns the block into prose.
|
|
368
|
+
const skillsDir = join(process.env.VELLUM_WORKSPACE_DIR!, "skills");
|
|
369
|
+
mkdirSync(join(skillsDir, "indented"), { recursive: true });
|
|
370
|
+
writeFileSync(
|
|
371
|
+
join(skillsDir, "indented", "SKILL.md"),
|
|
372
|
+
[
|
|
373
|
+
"---",
|
|
374
|
+
'name: "Indented"',
|
|
375
|
+
'description: "Opens with a code block"',
|
|
376
|
+
"---",
|
|
377
|
+
"",
|
|
378
|
+
" curl https://example.com/report",
|
|
379
|
+
"",
|
|
380
|
+
"Then read the output.",
|
|
381
|
+
"",
|
|
382
|
+
].join("\n"),
|
|
383
|
+
);
|
|
384
|
+
installMetaAuthors["indented"] = "assistant";
|
|
385
|
+
|
|
386
|
+
const result = await executeFindSimilarSkills(
|
|
387
|
+
{ goal: "fetch the report" },
|
|
388
|
+
makeRetrospectiveContext(),
|
|
389
|
+
{
|
|
390
|
+
nearestExistingSkills: async () => [{ skillId: "indented", score: 1 }],
|
|
391
|
+
loadCatalog: () =>
|
|
392
|
+
catalog({
|
|
393
|
+
id: "indented",
|
|
394
|
+
name: "Indented",
|
|
395
|
+
description: "Opens with a code block",
|
|
396
|
+
source: "managed",
|
|
397
|
+
}),
|
|
398
|
+
},
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
const { skills } = JSON.parse(result.content);
|
|
402
|
+
expect(skills[0].current.body_markdown).toBe(
|
|
403
|
+
" curl https://example.com/report\n\nThen read the output.",
|
|
404
|
+
);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("a skill that cannot be read drops current, not the hit", async () => {
|
|
408
|
+
installMetaAuthors["weekly-export"] = "assistant";
|
|
409
|
+
|
|
410
|
+
const result = await executeFindSimilarSkills(
|
|
411
|
+
{ goal: "export the weekly report" },
|
|
412
|
+
makeRetrospectiveContext(),
|
|
413
|
+
{
|
|
414
|
+
nearestExistingSkills: async () => [
|
|
415
|
+
{ skillId: "weekly-export", score: 0.9 },
|
|
416
|
+
],
|
|
417
|
+
loadCatalog: refinableCatalog,
|
|
418
|
+
readStoredManagedSkill: () => null,
|
|
419
|
+
},
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
const { skills } = JSON.parse(result.content);
|
|
423
|
+
expect(skills).toHaveLength(1);
|
|
424
|
+
expect(skills[0].skill_id).toBe("weekly-export");
|
|
425
|
+
expect(skills[0]).not.toHaveProperty("current");
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
220
429
|
describe("find_similar_skills — per-chat plugin scope", () => {
|
|
221
430
|
const SHORTLIST = [
|
|
222
431
|
{ skillId: "core-skill", score: 0.9 },
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import type { SkillSource } from "../../config/skills.js";
|
|
2
2
|
import { loadSkillCatalog } from "../../config/skills.js";
|
|
3
|
+
import { MEMORY_RETROSPECTIVE_ORIGIN } from "../../plugins/defaults/memory/memory-retrospective-constants.js";
|
|
3
4
|
import { nearestExistingSkills } from "../../plugins/defaults/memory/v3/candidate-match.js";
|
|
4
5
|
import { readInstallMeta } from "../../skills/install-meta.js";
|
|
5
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getManagedSkillDir,
|
|
8
|
+
readStoredManagedSkill,
|
|
9
|
+
type StoredManagedSkill,
|
|
10
|
+
} from "../../skills/managed-store.js";
|
|
6
11
|
import {
|
|
7
12
|
filterSkillsByPlatform,
|
|
8
13
|
type SkillPlatform,
|
|
@@ -21,6 +26,14 @@ import type { OwnerInfo, ToolContext, ToolExecutionResult } from "../types.js";
|
|
|
21
26
|
* `"user"` = a person wrote it, off-limits). It is undefined for non-managed
|
|
22
27
|
* sources and for managed skills with no recorded author, so the caller can
|
|
23
28
|
* distinguish its OWN managed skills from a user's without re-reading meta.
|
|
29
|
+
*
|
|
30
|
+
* `current` is the skill as stored, present only on a hit the caller may
|
|
31
|
+
* refine (managed, assistant-authored) and only for the retrospective. A
|
|
32
|
+
* refinement is a whole-file overwrite and the pass has no other read path:
|
|
33
|
+
* its `skill_load` grant covers skill-management alone, and loading would
|
|
34
|
+
* stamp `lastUsedAt` and count as usage. Fields are spelled as
|
|
35
|
+
* `scaffold_managed_skill`'s own arguments so the pass carries forward what
|
|
36
|
+
* it is not changing without translating names.
|
|
24
37
|
*/
|
|
25
38
|
interface EnrichedHit {
|
|
26
39
|
skill_id: string;
|
|
@@ -29,6 +42,19 @@ interface EnrichedHit {
|
|
|
29
42
|
source: SkillSource;
|
|
30
43
|
author?: "assistant" | "user";
|
|
31
44
|
score: number;
|
|
45
|
+
current?: CurrentSkill;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The refinable skill's present content, in `scaffold_managed_skill` argument names. */
|
|
49
|
+
interface CurrentSkill {
|
|
50
|
+
name: string;
|
|
51
|
+
description: string;
|
|
52
|
+
emoji?: string;
|
|
53
|
+
category?: string;
|
|
54
|
+
includes?: string[];
|
|
55
|
+
activation_hints?: string[];
|
|
56
|
+
avoid_when?: string[];
|
|
57
|
+
body_markdown: string;
|
|
32
58
|
}
|
|
33
59
|
|
|
34
60
|
/**
|
|
@@ -37,7 +63,8 @@ interface EnrichedHit {
|
|
|
37
63
|
* each joined to its catalog name/description. Exported so bundled-skill
|
|
38
64
|
* executors and tests can call it directly.
|
|
39
65
|
*
|
|
40
|
-
* `deps` injects the shortlist
|
|
66
|
+
* `deps` injects the shortlist, catalog, and stored-skill seams so tests run
|
|
67
|
+
* without Qdrant or a skills directory.
|
|
41
68
|
*/
|
|
42
69
|
export async function executeFindSimilarSkills(
|
|
43
70
|
input: Record<string, unknown>,
|
|
@@ -52,6 +79,7 @@ export async function executeFindSimilarSkills(
|
|
|
52
79
|
owner?: OwnerInfo;
|
|
53
80
|
platforms?: SkillPlatform[];
|
|
54
81
|
}[];
|
|
82
|
+
readStoredManagedSkill?: (skillId: string) => StoredManagedSkill | null;
|
|
55
83
|
} = {},
|
|
56
84
|
): Promise<ToolExecutionResult> {
|
|
57
85
|
const goal = input.goal;
|
|
@@ -79,6 +107,7 @@ export async function executeFindSimilarSkills(
|
|
|
79
107
|
|
|
80
108
|
const findNearest = deps.nearestExistingSkills ?? nearestExistingSkills;
|
|
81
109
|
const loadCatalog = deps.loadCatalog ?? (() => loadSkillCatalog());
|
|
110
|
+
const readStored = deps.readStoredManagedSkill ?? readStoredManagedSkill;
|
|
82
111
|
|
|
83
112
|
const catalog = loadCatalog();
|
|
84
113
|
const byId = new Map(catalog.map((s) => [s.id, s]));
|
|
@@ -114,6 +143,9 @@ export async function executeFindSimilarSkills(
|
|
|
114
143
|
...(context.signal ? { signal: context.signal } : {}),
|
|
115
144
|
});
|
|
116
145
|
|
|
146
|
+
const fromRetrospective =
|
|
147
|
+
context.requestOrigin === MEMORY_RETROSPECTIVE_ORIGIN;
|
|
148
|
+
|
|
117
149
|
const enriched: EnrichedHit[] = [];
|
|
118
150
|
for (const hit of hits) {
|
|
119
151
|
const skill = byId.get(hit.skillId);
|
|
@@ -126,19 +158,40 @@ export async function executeFindSimilarSkills(
|
|
|
126
158
|
if (outOfScope(skill)) {
|
|
127
159
|
continue;
|
|
128
160
|
}
|
|
161
|
+
// Join install-meta authorship for managed hits so the caller can tell its
|
|
162
|
+
// OWN skills (overwritable) from a user's. Best-effort: an absent/failed
|
|
163
|
+
// meta read leaves `author` undefined rather than throwing.
|
|
164
|
+
const author =
|
|
165
|
+
skill.source === "managed"
|
|
166
|
+
? readManagedSkillAuthor(hit.skillId)
|
|
167
|
+
: undefined;
|
|
168
|
+
// Only a refinable hit pays for the disk read, and a failed read drops
|
|
169
|
+
// `current` rather than the hit: the pass can still skip a skill it
|
|
170
|
+
// cannot see, it just cannot rewrite it well. Absent fields serialize
|
|
171
|
+
// away with the result.
|
|
172
|
+
const stored =
|
|
173
|
+
fromRetrospective && author === "assistant"
|
|
174
|
+
? readStored(hit.skillId)
|
|
175
|
+
: null;
|
|
129
176
|
enriched.push({
|
|
130
177
|
skill_id: hit.skillId,
|
|
131
178
|
name: skill.name,
|
|
132
179
|
description: skill.description,
|
|
133
180
|
source: skill.source,
|
|
134
|
-
|
|
135
|
-
// OWN skills (overwritable) from a user's. Best-effort: an absent/failed
|
|
136
|
-
// meta read leaves `author` undefined rather than throwing.
|
|
137
|
-
author:
|
|
138
|
-
skill.source === "managed"
|
|
139
|
-
? readManagedSkillAuthor(hit.skillId)
|
|
140
|
-
: undefined,
|
|
181
|
+
author,
|
|
141
182
|
score: hit.score,
|
|
183
|
+
current: stored
|
|
184
|
+
? {
|
|
185
|
+
name: stored.name,
|
|
186
|
+
description: stored.description,
|
|
187
|
+
emoji: stored.emoji,
|
|
188
|
+
category: stored.category,
|
|
189
|
+
includes: stored.includes,
|
|
190
|
+
activation_hints: stored.activationHints,
|
|
191
|
+
avoid_when: stored.avoidWhen,
|
|
192
|
+
body_markdown: stored.body,
|
|
193
|
+
}
|
|
194
|
+
: undefined,
|
|
142
195
|
});
|
|
143
196
|
}
|
|
144
197
|
|