@vellumai/assistant 0.5.2 → 0.5.3

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.
Files changed (108) hide show
  1. package/ARCHITECTURE.md +109 -0
  2. package/docs/skills.md +100 -0
  3. package/package.json +1 -1
  4. package/src/__tests__/conversation-agent-loop-overflow.test.ts +7 -0
  5. package/src/__tests__/conversation-agent-loop.test.ts +7 -0
  6. package/src/__tests__/conversation-memory-dirty-tail.test.ts +150 -0
  7. package/src/__tests__/conversation-provider-retry-repair.test.ts +7 -0
  8. package/src/__tests__/conversation-wipe.test.ts +226 -0
  9. package/src/__tests__/db-memory-archive-migration.test.ts +372 -0
  10. package/src/__tests__/db-memory-brief-state-migration.test.ts +213 -0
  11. package/src/__tests__/db-memory-reducer-checkpoints.test.ts +273 -0
  12. package/src/__tests__/inline-command-runner.test.ts +311 -0
  13. package/src/__tests__/inline-skill-authoring-guard.test.ts +220 -0
  14. package/src/__tests__/inline-skill-load-permissions.test.ts +435 -0
  15. package/src/__tests__/list-messages-attachments.test.ts +96 -0
  16. package/src/__tests__/memory-brief-open-loops.test.ts +530 -0
  17. package/src/__tests__/memory-brief-time.test.ts +285 -0
  18. package/src/__tests__/memory-brief-wrapper.test.ts +311 -0
  19. package/src/__tests__/memory-chunk-archive.test.ts +400 -0
  20. package/src/__tests__/memory-chunk-dual-write.test.ts +453 -0
  21. package/src/__tests__/memory-episode-archive.test.ts +370 -0
  22. package/src/__tests__/memory-episode-dual-write.test.ts +626 -0
  23. package/src/__tests__/memory-observation-archive.test.ts +375 -0
  24. package/src/__tests__/memory-observation-dual-write.test.ts +318 -0
  25. package/src/__tests__/memory-recall-quality.test.ts +2 -2
  26. package/src/__tests__/memory-reducer-store.test.ts +728 -0
  27. package/src/__tests__/memory-reducer-types.test.ts +699 -0
  28. package/src/__tests__/memory-reducer.test.ts +698 -0
  29. package/src/__tests__/memory-regressions.test.ts +6 -4
  30. package/src/__tests__/memory-simplified-config.test.ts +281 -0
  31. package/src/__tests__/parse-identity-fields.test.ts +129 -0
  32. package/src/__tests__/skill-load-inline-command.test.ts +598 -0
  33. package/src/__tests__/skill-load-inline-includes.test.ts +644 -0
  34. package/src/__tests__/skills-inline-command-expansions.test.ts +301 -0
  35. package/src/__tests__/skills-transitive-hash.test.ts +333 -0
  36. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +320 -0
  37. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +4 -4
  38. package/src/config/bundled-skills/app-builder/SKILL.md +8 -8
  39. package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
  40. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  41. package/src/config/feature-flag-registry.json +16 -0
  42. package/src/config/loader.ts +1 -0
  43. package/src/config/raw-config-utils.ts +28 -0
  44. package/src/config/schema.ts +12 -0
  45. package/src/config/schemas/memory-simplified.ts +101 -0
  46. package/src/config/schemas/memory.ts +4 -0
  47. package/src/config/skills.ts +50 -4
  48. package/src/daemon/conversation-agent-loop-handlers.ts +8 -3
  49. package/src/daemon/conversation-agent-loop.ts +71 -1
  50. package/src/daemon/conversation-lifecycle.ts +11 -1
  51. package/src/daemon/conversation-runtime-assembly.ts +2 -1
  52. package/src/daemon/conversation-surfaces.ts +31 -8
  53. package/src/daemon/conversation.ts +40 -23
  54. package/src/daemon/handlers/config-embeddings.ts +10 -2
  55. package/src/daemon/handlers/config-model.ts +0 -9
  56. package/src/daemon/handlers/identity.ts +12 -1
  57. package/src/daemon/lifecycle.ts +9 -1
  58. package/src/daemon/message-types/conversations.ts +0 -1
  59. package/src/daemon/server.ts +1 -1
  60. package/src/followups/followup-store.ts +47 -1
  61. package/src/memory/archive-store.ts +400 -0
  62. package/src/memory/brief-formatting.ts +33 -0
  63. package/src/memory/brief-open-loops.ts +266 -0
  64. package/src/memory/brief-time.ts +161 -0
  65. package/src/memory/brief.ts +75 -0
  66. package/src/memory/conversation-crud.ts +245 -101
  67. package/src/memory/db-init.ts +12 -0
  68. package/src/memory/indexer.ts +106 -15
  69. package/src/memory/job-handlers/embedding.test.ts +1 -0
  70. package/src/memory/job-handlers/embedding.ts +83 -0
  71. package/src/memory/job-utils.ts +1 -1
  72. package/src/memory/jobs-store.ts +6 -0
  73. package/src/memory/jobs-worker.ts +12 -0
  74. package/src/memory/migrations/185-memory-brief-state.ts +52 -0
  75. package/src/memory/migrations/186-memory-archive.ts +109 -0
  76. package/src/memory/migrations/187-memory-reducer-checkpoints.ts +19 -0
  77. package/src/memory/migrations/index.ts +3 -0
  78. package/src/memory/qdrant-client.ts +23 -4
  79. package/src/memory/reducer-store.ts +271 -0
  80. package/src/memory/reducer-types.ts +99 -0
  81. package/src/memory/reducer.ts +453 -0
  82. package/src/memory/schema/conversations.ts +3 -0
  83. package/src/memory/schema/index.ts +2 -0
  84. package/src/memory/schema/memory-archive.ts +121 -0
  85. package/src/memory/schema/memory-brief.ts +55 -0
  86. package/src/memory/search/semantic.ts +17 -4
  87. package/src/oauth/oauth-store.ts +3 -1
  88. package/src/permissions/checker.ts +89 -6
  89. package/src/permissions/defaults.ts +14 -0
  90. package/src/runtime/routes/conversation-management-routes.ts +6 -0
  91. package/src/runtime/routes/conversation-query-routes.ts +7 -0
  92. package/src/runtime/routes/conversation-routes.ts +52 -5
  93. package/src/runtime/routes/identity-routes.ts +2 -35
  94. package/src/runtime/routes/llm-context-normalization.ts +14 -1
  95. package/src/runtime/routes/memory-item-routes.ts +90 -5
  96. package/src/runtime/routes/secret-routes.ts +2 -0
  97. package/src/runtime/routes/surface-action-routes.ts +68 -1
  98. package/src/schedule/schedule-store.ts +21 -0
  99. package/src/skills/inline-command-expansions.ts +204 -0
  100. package/src/skills/inline-command-render.ts +127 -0
  101. package/src/skills/inline-command-runner.ts +242 -0
  102. package/src/skills/transitive-version-hash.ts +88 -0
  103. package/src/tasks/task-store.ts +43 -1
  104. package/src/tools/permission-checker.ts +8 -1
  105. package/src/tools/skills/load.ts +140 -6
  106. package/src/util/platform.ts +18 -0
  107. package/src/workspace/migrations/{002-backfill-installation-id.ts → 011-backfill-installation-id.ts} +1 -1
  108. package/src/workspace/migrations/registry.ts +1 -1
@@ -1,4 +1,4 @@
1
- import { desc, eq, inArray } from "drizzle-orm";
1
+ import { asc, desc, eq, inArray, or } from "drizzle-orm";
2
2
 
3
3
  import { getDb } from "../memory/db.js";
4
4
  import { taskRuns, tasks, workItems } from "../memory/schema.js";
@@ -139,3 +139,45 @@ export function getTaskRun(id: string): TaskRun | undefined {
139
139
  const db = getDb();
140
140
  return db.select().from(taskRuns).where(eq(taskRuns.id, id)).get();
141
141
  }
142
+
143
+ // ── Brief Helpers ─────────────────────────────────────────────────────
144
+
145
+ /**
146
+ * Lightweight read-only projection of a work item used by the brief compiler.
147
+ * Avoids pulling the full WorkItem type with all its tool/approval fields.
148
+ */
149
+ export interface ActionableWorkItem {
150
+ id: string;
151
+ taskId: string;
152
+ title: string;
153
+ status: string;
154
+ priorityTier: number;
155
+ updatedAt: number;
156
+ }
157
+
158
+ /**
159
+ * Return actionable work items — those that are queued, running, or
160
+ * awaiting review. Ordered by priority (high first) then most-recently-updated.
161
+ */
162
+ export function getActionableWorkItems(): ActionableWorkItem[] {
163
+ const db = getDb();
164
+ return db
165
+ .select({
166
+ id: workItems.id,
167
+ taskId: workItems.taskId,
168
+ title: workItems.title,
169
+ status: workItems.status,
170
+ priorityTier: workItems.priorityTier,
171
+ updatedAt: workItems.updatedAt,
172
+ })
173
+ .from(workItems)
174
+ .where(
175
+ or(
176
+ eq(workItems.status, "queued"),
177
+ eq(workItems.status, "running"),
178
+ eq(workItems.status, "awaiting_review"),
179
+ ),
180
+ )
181
+ .orderBy(asc(workItems.priorityTier), desc(workItems.updatedAt))
182
+ .all();
183
+ }
@@ -142,10 +142,17 @@ export class PermissionChecker {
142
142
  // is the owner - prompting makes no sense when there is no client.
143
143
  // Exception: requireFreshApproval tools cannot be auto-approved -
144
144
  // without a human present, bundle installation must be denied.
145
+ // Exception: inline-command skill loads (skill_load_dynamic:*) must
146
+ // never be silently auto-approved — they execute embedded commands
147
+ // and require explicit human review or a pinned trust rule.
148
+ const isDynamicSkillLoad =
149
+ result.matchedRule?.pattern.startsWith("skill_load_dynamic:") ===
150
+ true;
145
151
  if (
146
152
  context.isInteractive === false &&
147
153
  context.trustClass === "guardian" &&
148
- !context.requireFreshApproval
154
+ !context.requireFreshApproval &&
155
+ !isDynamicSkillLoad
149
156
  ) {
150
157
  log.info(
151
158
  { toolName: name, riskLevel },
@@ -21,12 +21,24 @@ import {
21
21
  indexCatalogById,
22
22
  validateIncludes,
23
23
  } from "../../skills/include-graph.js";
24
+ import { renderInlineCommands } from "../../skills/inline-command-render.js";
24
25
  import { parseToolManifestFile } from "../../skills/tool-manifest.js";
25
26
  import { computeSkillVersionHash } from "../../skills/version-hash.js";
26
27
  import { getLogger } from "../../util/logger.js";
28
+ import { getWorkspaceDirDisplay } from "../../util/platform.js";
27
29
  import { registerTool } from "../registry.js";
28
30
  import type { Tool, ToolContext, ToolExecutionResult } from "../types.js";
29
31
 
32
+ /** Canonical feature flag key for inline skill command expansion. */
33
+ const INLINE_COMMANDS_FLAG_KEY = "feature_flags.inline-skill-commands.enabled";
34
+
35
+ /** Skill sources eligible for inline command expansion in v1. */
36
+ const INLINE_COMMAND_ELIGIBLE_SOURCES = new Set([
37
+ "bundled",
38
+ "managed",
39
+ "workspace",
40
+ ]);
41
+
30
42
  const log = getLogger("skill-load");
31
43
 
32
44
  /**
@@ -76,7 +88,9 @@ function formatToolSchemas(
76
88
 
77
89
  for (const tool of manifest.tools) {
78
90
  lines.push(`${toolHeadingLevel} ${tool.name}`);
79
- lines.push(tool.description);
91
+ lines.push(
92
+ tool.description.replaceAll("{workspaceDir}", getWorkspaceDirDisplay()),
93
+ );
80
94
 
81
95
  const schema = tool.input_schema;
82
96
  const properties = schema.properties as
@@ -96,7 +110,7 @@ function formatToolSchemas(
96
110
  : "optional";
97
111
  const descPart =
98
112
  typeof paramDef.description === "string"
99
- ? `: ${paramDef.description}`
113
+ ? `: ${paramDef.description.replaceAll("{workspaceDir}", getWorkspaceDirDisplay())}`
100
114
  : "";
101
115
  lines.push(
102
116
  `- ${paramName} (${paramType}, ${requiredLabel})${descPart}`,
@@ -113,7 +127,7 @@ function formatToolSchemas(
113
127
  export class SkillLoadTool implements Tool {
114
128
  name = "skill_load";
115
129
  description =
116
- "Load full instructions for a skill. Works for both bundled skills (listed in the catalog) and workspace skills in ~/.vellum/workspace/skills.";
130
+ "Load full instructions for a skill. Works for both bundled skills (listed in the catalog) and custom workspace skills.";
117
131
  category = "skills";
118
132
  defaultRiskLevel = RiskLevel.Low;
119
133
 
@@ -136,7 +150,7 @@ export class SkillLoadTool implements Tool {
136
150
 
137
151
  async execute(
138
152
  input: Record<string, unknown>,
139
- _context: ToolContext,
153
+ context: ToolContext,
140
154
  ): Promise<ToolExecutionResult> {
141
155
  const selector = input.skill;
142
156
  if (typeof selector !== "string" || selector.trim().length === 0) {
@@ -279,7 +293,63 @@ export class SkillLoadTool implements Tool {
279
293
  }
280
294
  }
281
295
 
282
- const body = skill.body.length > 0 ? skill.body : "(No body content)";
296
+ let body = skill.body.length > 0 ? skill.body : "(No body content)";
297
+
298
+ // ── Inline command expansion ──────────────────────────────────────────
299
+ const hasInlineCommands =
300
+ skill.inlineCommandExpansions && skill.inlineCommandExpansions.length > 0;
301
+
302
+ if (hasInlineCommands) {
303
+ const inlineFlagEnabled = isAssistantFeatureFlagEnabled(
304
+ INLINE_COMMANDS_FLAG_KEY,
305
+ config,
306
+ );
307
+
308
+ if (!inlineFlagEnabled) {
309
+ // Feature flag is off: fail closed instead of leaving live tokens in
310
+ // the prompt that the LLM might try to interpret.
311
+ return {
312
+ content: `Error: skill "${skill.id}" contains inline command expansions but the inline-skill-commands feature flag is disabled. Enable the flag to use this skill.`,
313
+ isError: true,
314
+ };
315
+ }
316
+
317
+ if (skill.source === "extra") {
318
+ // Third-party extra roots are out of scope for inline command
319
+ // expansion in v1. Reject explicitly so the failure is clear.
320
+ return {
321
+ content: `Error: skill "${skill.id}" contains inline command expansions but inline commands are not supported for third-party (extra) skill sources.`,
322
+ isError: true,
323
+ };
324
+ }
325
+
326
+ if (!INLINE_COMMAND_ELIGIBLE_SOURCES.has(skill.source)) {
327
+ // Defensive: reject any other unknown sources that somehow have
328
+ // inline commands. Should not happen with current SkillSource values,
329
+ // but fail closed if a new source type is added without updating this.
330
+ return {
331
+ content: `Error: skill "${skill.id}" contains inline command expansions but source "${skill.source}" is not eligible for inline command expansion.`,
332
+ isError: true,
333
+ };
334
+ }
335
+
336
+ // Render inline commands by executing each through the sandbox runner
337
+ const renderResult = await renderInlineCommands(
338
+ body,
339
+ skill.inlineCommandExpansions!,
340
+ context.workingDir,
341
+ );
342
+ body = renderResult.renderedBody;
343
+
344
+ log.info(
345
+ {
346
+ skillId: skill.id,
347
+ expandedCount: renderResult.expandedCount,
348
+ failedCount: renderResult.failedCount,
349
+ },
350
+ "Rendered inline command expansions",
351
+ );
352
+ }
283
353
 
284
354
  // Build reference file listing (if any)
285
355
  const referenceListing = listReferenceFiles(skill.directoryPath);
@@ -313,8 +383,72 @@ export class SkillLoadTool implements Tool {
313
383
  // Load the included skill's body content
314
384
  const childLoaded = loadSkillBySelector(childId);
315
385
  if (childLoaded.skill && childLoaded.skill.body.length > 0) {
386
+ let childBody = childLoaded.skill.body;
387
+
388
+ // ── Inline command expansion for included child skill ─────────
389
+ const childHasInlineCommands =
390
+ childLoaded.skill.inlineCommandExpansions &&
391
+ childLoaded.skill.inlineCommandExpansions.length > 0;
392
+
393
+ if (childHasInlineCommands) {
394
+ const childInlineFlagEnabled = isAssistantFeatureFlagEnabled(
395
+ INLINE_COMMANDS_FLAG_KEY,
396
+ config,
397
+ );
398
+
399
+ // Fail closed: if the flag is off, reject the entire skill_load
400
+ // just like we do for root skills. Leaving raw !`...` tokens in
401
+ // the prompt would violate the documented fail-closed contract.
402
+ if (!childInlineFlagEnabled) {
403
+ return {
404
+ content: `Error: included skill "${childId}" contains inline command expansions but the inline-skill-commands feature flag is disabled. Enable the flag to use skill "${skill.id}".`,
405
+ isError: true,
406
+ };
407
+ }
408
+
409
+ if (childLoaded.skill.source === "extra") {
410
+ return {
411
+ content: `Error: included skill "${childId}" contains inline command expansions but inline commands are not supported for third-party (extra) skill sources.`,
412
+ isError: true,
413
+ };
414
+ }
415
+
416
+ if (
417
+ !INLINE_COMMAND_ELIGIBLE_SOURCES.has(childLoaded.skill.source)
418
+ ) {
419
+ return {
420
+ content: `Error: included skill "${childId}" contains inline command expansions but source "${childLoaded.skill.source}" is not eligible for inline command expansion.`,
421
+ isError: true,
422
+ };
423
+ }
424
+
425
+ try {
426
+ const childRenderResult = await renderInlineCommands(
427
+ childBody,
428
+ childLoaded.skill.inlineCommandExpansions!,
429
+ context.workingDir,
430
+ );
431
+ childBody = childRenderResult.renderedBody;
432
+
433
+ log.info(
434
+ {
435
+ skillId: childId,
436
+ parentSkillId: skill.id,
437
+ expandedCount: childRenderResult.expandedCount,
438
+ failedCount: childRenderResult.failedCount,
439
+ },
440
+ "Rendered inline command expansions for included skill",
441
+ );
442
+ } catch (err) {
443
+ log.warn(
444
+ { err, skillId: childId, parentSkillId: skill.id },
445
+ "Failed to render inline commands for included skill, using raw body",
446
+ );
447
+ }
448
+ }
449
+
316
450
  includedBodies.push(
317
- `--- Included Skill: ${childLoaded.skill.displayName} (${childId}) ---\n${childLoaded.skill.body}`,
451
+ `--- Included Skill: ${childLoaded.skill.displayName} (${childId}) ---\n${childBody}`,
318
452
  );
319
453
 
320
454
  // List reference files for the included skill
@@ -366,6 +366,24 @@ export function getWorkspaceDir(): string {
366
366
  return join(getRootDir(), "workspace");
367
367
  }
368
368
 
369
+ /**
370
+ * Returns a display-friendly workspace path for embedding in agent-facing text
371
+ * (skill bodies, tool descriptions). Replaces the home directory prefix with `~`
372
+ * so paths stay concise and portable across machines.
373
+ *
374
+ * Examples:
375
+ * /Users/sidd/.vellum/workspace → ~/.vellum/workspace
376
+ * /data/.vellum/workspace → /data/.vellum/workspace
377
+ */
378
+ export function getWorkspaceDirDisplay(): string {
379
+ const abs = getWorkspaceDir();
380
+ const home = homedir();
381
+ if (abs.startsWith(home + "/") || abs === home) {
382
+ return "~" + abs.slice(home.length);
383
+ }
384
+ return abs;
385
+ }
386
+
369
387
  /** Returns ~/.vellum/workspace/config.json */
370
388
  export function getWorkspaceConfigPath(): string {
371
389
  return join(getWorkspaceDir(), "config.json");
@@ -11,7 +11,7 @@ import { getExternalAssistantId } from "../../runtime/auth/external-assistant-id
11
11
  import type { WorkspaceMigration } from "./types.js";
12
12
 
13
13
  export const backfillInstallationIdMigration: WorkspaceMigration = {
14
- id: "002-backfill-installation-id",
14
+ id: "011-backfill-installation-id",
15
15
  description:
16
16
  "Backfill installationId into lockfile from SQLite checkpoint and clean up stale row",
17
17
  run(_workspaceDir: string): void {
@@ -1,5 +1,4 @@
1
1
  import { avatarRenameMigration } from "./001-avatar-rename.js";
2
- import { backfillInstallationIdMigration } from "./002-backfill-installation-id.js";
3
2
  import { seedDeviceIdMigration } from "./003-seed-device-id.js";
4
3
  import { extractCollectUsageDataMigration } from "./004-extract-collect-usage-data.js";
5
4
  import { addSendDiagnosticsMigration } from "./005-add-send-diagnostics.js";
@@ -8,6 +7,7 @@ import { webSearchProviderRenameMigration } from "./007-web-search-provider-rena
8
7
  import { voiceTimeoutAndMaxStepsMigration } from "./008-voice-timeout-and-max-steps.js";
9
8
  import { backfillConversationDiskViewMigration } from "./009-backfill-conversation-disk-view.js";
10
9
  import { appDirRenameMigration } from "./010-app-dir-rename.js";
10
+ import { backfillInstallationIdMigration } from "./011-backfill-installation-id.js";
11
11
  import { renameConversationDiskViewDirsMigration } from "./012-rename-conversation-disk-view-dirs.js";
12
12
  import { repairConversationDiskViewMigration } from "./013-repair-conversation-disk-view.js";
13
13
  import type { WorkspaceMigration } from "./types.js";