@vellumai/assistant 0.9.0 → 0.9.1-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.
Files changed (222) hide show
  1. package/ARCHITECTURE.md +18 -34
  2. package/bun.lock +7 -8
  3. package/docs/activation-funnel-telemetry.md +4 -4
  4. package/docs/architecture/security.md +29 -28
  5. package/docs/stt-provider-onboarding.md +3 -5
  6. package/docs/workflows-testing.md +13 -44
  7. package/docs/workflows.md +3 -5
  8. package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
  9. package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
  10. package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
  11. package/node_modules/@vellumai/gateway-client/src/index.ts +17 -6
  12. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
  13. package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
  14. package/openapi.yaml +135 -59
  15. package/package.json +2 -1
  16. package/scripts/sync-llm-catalog.ts +6 -15
  17. package/scripts/sync-web-search-catalog.ts +3 -11
  18. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +14 -26
  19. package/src/__tests__/agent-loop-compaction-strip.test.ts +240 -0
  20. package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
  21. package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
  22. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
  23. package/src/__tests__/app-dir-path-guard.test.ts +0 -1
  24. package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
  25. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
  26. package/src/__tests__/avatar-identity-sync.test.ts +2 -27
  27. package/src/__tests__/btw-routes.test.ts +6 -8
  28. package/src/__tests__/checker.test.ts +0 -3
  29. package/src/__tests__/config-loader-backfill.test.ts +103 -6
  30. package/src/__tests__/config-watcher.test.ts +0 -18
  31. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
  32. package/src/__tests__/credential-broker.test.ts +449 -1
  33. package/src/__tests__/credential-execution-tools.test.ts +0 -1
  34. package/src/__tests__/credential-prompt-route.test.ts +3 -4
  35. package/src/__tests__/credential-routes.test.ts +360 -0
  36. package/src/__tests__/credential-security-invariants.test.ts +2 -13
  37. package/src/__tests__/dynamic-page-surface.test.ts +101 -1
  38. package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
  39. package/src/__tests__/gateway-only-guard.test.ts +3 -7
  40. package/src/__tests__/identity-routes.test.ts +0 -189
  41. package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
  42. package/src/__tests__/invite-redemption-service.test.ts +4 -4
  43. package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
  44. package/src/__tests__/llm-catalog-parity.test.ts +0 -22
  45. package/src/__tests__/llm-resolver.test.ts +49 -24
  46. package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
  47. package/src/__tests__/onboarding-persona-write.test.ts +1 -1
  48. package/src/__tests__/persona-resolver.test.ts +11 -14
  49. package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
  50. package/src/__tests__/registry.test.ts +2 -7
  51. package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
  52. package/src/__tests__/schedule-routes.test.ts +0 -30
  53. package/src/__tests__/schedule-tools.test.ts +2 -18
  54. package/src/__tests__/skill-execute-input.test.ts +46 -1
  55. package/src/__tests__/skill-runtime-path.test.ts +2 -3
  56. package/src/__tests__/subagent-tools.test.ts +116 -0
  57. package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
  58. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
  59. package/src/__tests__/token-manager.test.ts +519 -0
  60. package/src/__tests__/tool-executor.test.ts +0 -79
  61. package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
  62. package/src/__tests__/trusted-contact-verification.test.ts +6 -6
  63. package/src/__tests__/voice-invite-redemption.test.ts +2 -2
  64. package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
  65. package/src/__tests__/workspace-greetings.test.ts +152 -0
  66. package/src/agent/loop.ts +25 -5
  67. package/src/api/README.md +6 -6
  68. package/src/api/responses/conversation-message.ts +2 -4
  69. package/src/api/responses/home.ts +0 -4
  70. package/src/approvals/guardian-request-resolvers.ts +2 -2
  71. package/src/calls/relay-access-wait.ts +1 -1
  72. package/src/calls/voice-session-bridge.ts +2 -2
  73. package/src/cli/commands/plugins.ts +143 -2
  74. package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
  75. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +313 -0
  76. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +253 -2
  77. package/src/cli/lib/diff-plugin.ts +346 -0
  78. package/src/cli/lib/install-from-github.ts +105 -17
  79. package/src/cli/lib/merge-plugin-tree.ts +228 -0
  80. package/src/cli/lib/plugin-fingerprint.ts +14 -0
  81. package/src/cli/lib/upgrade-plugin.ts +270 -10
  82. package/src/cli/program.ts +0 -2
  83. package/src/config/bundled-skills/subagent/SKILL.md +4 -0
  84. package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
  85. package/src/config/bundled-skills/workflows/SKILL.md +0 -1
  86. package/src/config/bundled-tool-registry.ts +2 -7
  87. package/src/config/call-site-defaults.ts +12 -2
  88. package/src/config/feature-flag-registry.json +1 -17
  89. package/src/config/inference-profile-validation.ts +26 -0
  90. package/src/config/loader.ts +4 -0
  91. package/src/config/profile-order.ts +28 -0
  92. package/src/config/schemas/elevenlabs.ts +0 -1
  93. package/src/config/schemas/platform.ts +0 -8
  94. package/src/config/seed-inference-profiles.ts +25 -20
  95. package/src/contacts/contact-store.ts +87 -96
  96. package/src/contacts/contacts-write.ts +5 -21
  97. package/src/context/compactor.ts +2 -2
  98. package/src/credential-execution/process-manager.ts +55 -14
  99. package/src/credential-execution/prompted-credential.ts +2 -3
  100. package/src/daemon/config-watcher.ts +0 -4
  101. package/src/daemon/conversation-agent-loop.ts +15 -4
  102. package/src/daemon/conversation-slash.ts +2 -23
  103. package/src/daemon/conversation-tool-setup.ts +11 -3
  104. package/src/daemon/conversation.ts +2 -0
  105. package/src/daemon/handlers/config-channels.ts +20 -16
  106. package/src/daemon/handlers/config-slack-channel.ts +2 -3
  107. package/src/daemon/lifecycle.ts +0 -7
  108. package/src/daemon/message-types/conversations.ts +3 -3
  109. package/src/daemon/message-types/sync.ts +0 -1
  110. package/src/daemon/orphan-reaper.test.ts +0 -19
  111. package/src/daemon/orphan-reaper.ts +2 -24
  112. package/src/daemon/server.ts +0 -10
  113. package/src/home/relationship-state.ts +2 -4
  114. package/src/memory/__tests__/memory-retrospective-job.test.ts +195 -401
  115. package/src/memory/bookmark-crud.ts +1 -2
  116. package/src/memory/db-init.ts +7 -17
  117. package/src/memory/embedding-backend.ts +23 -0
  118. package/src/memory/embedding-billing-breaker.ts +96 -0
  119. package/src/memory/jobs-store.ts +25 -13
  120. package/src/memory/jobs-worker.ts +52 -0
  121. package/src/memory/memory-retrospective-constants.ts +4 -4
  122. package/src/memory/memory-retrospective-job.ts +19 -227
  123. package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +62 -0
  124. package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +311 -0
  125. package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
  126. package/src/memory/migrations/index.ts +1 -0
  127. package/src/memory/migrations/run-migrations.ts +41 -0
  128. package/src/memory/migrations/validate-migration-state.ts +1 -1
  129. package/src/memory/schema/contacts.ts +0 -4
  130. package/src/messaging/providers/slack/adapter.ts +1 -1
  131. package/src/notifications/adapters/shared.ts +29 -0
  132. package/src/notifications/adapters/slack.ts +5 -32
  133. package/src/notifications/adapters/telegram.ts +2 -20
  134. package/src/notifications/broadcaster.ts +10 -1
  135. package/src/notifications/home-feed-side-effect.ts +4 -3
  136. package/src/notifications/notification-utils.ts +17 -19
  137. package/src/notifications/types.ts +7 -0
  138. package/src/oauth/AGENTS.md +5 -24
  139. package/src/plugin-api/constants.ts +1 -1
  140. package/src/plugin-api/index.ts +6 -1
  141. package/src/plugin-api/model-profiles.ts +33 -0
  142. package/src/plugin-api/types.ts +50 -2
  143. package/src/plugins/defaults/index.ts +25 -0
  144. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +54 -2
  145. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +107 -7
  146. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
  147. package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
  148. package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
  149. package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
  150. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +1 -1
  151. package/src/prompts/persona-resolver.ts +2 -2
  152. package/src/runtime/AGENTS.md +0 -1
  153. package/src/runtime/actor-trust-resolver.ts +12 -44
  154. package/src/runtime/btw-sidechain.ts +3 -6
  155. package/src/runtime/channel-approval-types.ts +18 -45
  156. package/src/runtime/channel-invite-transports/telegram.ts +4 -4
  157. package/src/runtime/channel-verification-service.ts +4 -3
  158. package/src/runtime/invite-redemption-service.ts +3 -3
  159. package/src/runtime/routes/__tests__/plugins-routes.test.ts +218 -1
  160. package/src/runtime/routes/app-routes.ts +1 -1
  161. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
  162. package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
  163. package/src/runtime/routes/btw-routes.ts +1 -27
  164. package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
  165. package/src/runtime/routes/conversation-routes.ts +2 -2
  166. package/src/runtime/routes/credential-routes.ts +40 -16
  167. package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
  168. package/src/runtime/routes/identity-routes.ts +1 -296
  169. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +1 -1
  170. package/src/runtime/routes/plugins-routes.ts +171 -5
  171. package/src/runtime/routes/schedule-routes.ts +0 -22
  172. package/src/runtime/routes/workflow-routes.test.ts +4 -43
  173. package/src/runtime/routes/workflow-routes.ts +0 -28
  174. package/src/runtime/routes/workspace-greetings.ts +55 -0
  175. package/src/runtime/sync/resource-sync-events.ts +1 -11
  176. package/src/schedule/inference-profile.ts +2 -14
  177. package/src/subagent/manager.ts +6 -0
  178. package/src/subagent/types.ts +6 -0
  179. package/src/tools/AGENTS.md +3 -3
  180. package/src/tools/browser/browser-execution.ts +1 -1
  181. package/src/tools/network/web-search-error.ts +1 -1
  182. package/src/tools/permission-checker.ts +1 -1
  183. package/src/tools/schedule/create.ts +3 -9
  184. package/src/tools/schedule/update.ts +2 -10
  185. package/src/tools/side-effects.ts +2 -17
  186. package/src/tools/skills/execute.ts +34 -0
  187. package/src/tools/subagent/spawn.ts +34 -9
  188. package/src/tools/tool-approval-handler.ts +1 -3
  189. package/src/tools/tool-manifest.ts +0 -2
  190. package/src/tools/ui-surface/definitions.ts +39 -1
  191. package/src/tools/workflows/run-workflow.test.ts +8 -18
  192. package/src/util/platform.ts +2 -2
  193. package/src/workflows/capabilities.ts +2 -3
  194. package/src/workflows/run-manager.test.ts +0 -25
  195. package/src/workflows/run-manager.ts +2 -24
  196. package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
  197. package/src/__tests__/credential-security-e2e.test.ts +0 -362
  198. package/src/__tests__/credential-vault-unit.test.ts +0 -1528
  199. package/src/__tests__/credential-vault.test.ts +0 -1706
  200. package/src/__tests__/identity-intro-cache.test.ts +0 -315
  201. package/src/__tests__/secret-onetime-send.test.ts +0 -182
  202. package/src/cli/commands/__tests__/task.test.ts +0 -914
  203. package/src/cli/commands/task.ts +0 -771
  204. package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
  205. package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
  206. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
  207. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
  208. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
  209. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
  210. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
  211. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
  212. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
  213. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
  214. package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
  215. package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
  216. package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
  217. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
  218. package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
  219. package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
  220. package/src/memory/preloaded-apps.ts +0 -116
  221. package/src/runtime/routes/identity-intro-cache.ts +0 -172
  222. package/src/tools/credentials/vault.ts +0 -712
@@ -180,7 +180,7 @@ describe("redeemVoiceInviteCode", () => {
180
180
 
181
181
  const channelResult = findContactChannel({
182
182
  channelType: "phone",
183
- externalUserId: phone,
183
+ address: phone,
184
184
  });
185
185
 
186
186
  expect(channelResult).not.toBeNull();
@@ -399,7 +399,7 @@ describe("redeemVoiceInviteCode", () => {
399
399
  // Verify the redeemer's phone is now bound to Mom's contact
400
400
  const contactResult = findContactChannel({
401
401
  channelType: "phone",
402
- externalUserId: phone,
402
+ address: phone,
403
403
  });
404
404
  expect(contactResult).not.toBeNull();
405
405
  expect(contactResult!.contact.id).toBe(momContact.id);
@@ -8,20 +8,15 @@ import {
8
8
  } from "../providers/search-provider-catalog.js";
9
9
 
10
10
  /**
11
- * Parity guard: daemon TS catalog vs the two generated JSON copies.
11
+ * Parity guard: daemon TS catalog vs the generated JSON copy.
12
12
  *
13
13
  * The daemon maintains its canonical search-provider catalog in
14
14
  * `assistant/src/providers/search-provider-catalog.ts`.
15
- * `assistant/scripts/sync-web-search-catalog.ts` writes two byte-identical
16
- * artifacts:
17
- * - `meta/web-search-provider-catalog.json` — primary cross-package artifact.
18
- * - `clients/shared/Resources/web-search-provider-catalog.json` — SwiftPM
19
- * resource bundled into `VellumAssistantShared` (Swift cannot reach
20
- * files outside a target's source directory).
15
+ * `assistant/scripts/sync-web-search-catalog.ts` writes
16
+ * `meta/web-search-provider-catalog.json` — the primary cross-package artifact.
21
17
  *
22
18
  * These tests enforce structural equality between the TS catalog and the
23
- * meta/ copy, plus byte equality between the meta/ copy and the SwiftPM
24
- * mirror. CI fails when any of the three drift.
19
+ * meta/ copy. CI fails when they drift.
25
20
  */
26
21
 
27
22
  interface JsonCatalogEntry {
@@ -46,11 +41,6 @@ const META_JSON_PATH = join(
46
41
  "..",
47
42
  "meta/web-search-provider-catalog.json",
48
43
  );
49
- const SWIFTPM_MIRROR_PATH = join(
50
- process.cwd(),
51
- "..",
52
- "clients/shared/Resources/web-search-provider-catalog.json",
53
- );
54
44
 
55
45
  function loadJsonCatalog(): JsonCatalog {
56
46
  return JSON.parse(readFileSync(META_JSON_PATH, "utf-8"));
@@ -73,7 +63,8 @@ function entryToPlain(
73
63
  if (entry.apiKeyPrefix !== undefined) out.apiKeyPrefix = entry.apiKeyPrefix;
74
64
  if (entry.envVar !== undefined) out.envVar = entry.envVar;
75
65
  if (entry.secretKey !== undefined) out.secretKey = entry.secretKey;
76
- if (entry.fallbackOrder !== undefined) out.fallbackOrder = entry.fallbackOrder;
66
+ if (entry.fallbackOrder !== undefined)
67
+ out.fallbackOrder = entry.fallbackOrder;
77
68
  if (entry.privacyPolicyUrl !== undefined) {
78
69
  out.privacyPolicyUrl = entry.privacyPolicyUrl;
79
70
  }
@@ -95,14 +86,4 @@ describe("web-search catalog parity (TS canonical vs meta/ JSON)", () => {
95
86
  SEARCH_PROVIDER_CATALOG.map((p) => p.id),
96
87
  );
97
88
  });
98
-
99
- test("SwiftPM mirror is byte-identical to meta/ copy", () => {
100
- // The generator writes both files from the same serializer; this guard
101
- // catches any case where one copy is regenerated without the other.
102
- // Byte equality is required because SwiftPM bundles the resource verbatim
103
- // and the meta/ JSON is consumed as a cross-package artifact.
104
- const metaBytes = readFileSync(META_JSON_PATH);
105
- const swiftPmBytes = readFileSync(SWIFTPM_MIRROR_PATH);
106
- expect(swiftPmBytes.equals(metaBytes)).toBe(true);
107
- });
108
89
  });
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Unit tests for workspace-authored greeting parsing.
3
+ */
4
+
5
+ import { afterEach, describe, expect, mock, test } from "bun:test";
6
+
7
+ const workspaceFiles: Record<string, string> = {};
8
+
9
+ mock.module("node:fs", () => ({
10
+ existsSync: (path: string) => {
11
+ const name = path.split("/").pop() ?? "";
12
+ return name in workspaceFiles;
13
+ },
14
+ readFileSync: (path: string, _encoding: string) => {
15
+ const name = path.split("/").pop() ?? "";
16
+ if (name in workspaceFiles) return workspaceFiles[name];
17
+ throw new Error(`ENOENT: ${path}`);
18
+ },
19
+ }));
20
+
21
+ import {
22
+ parseGreetingsSection,
23
+ readWorkspaceGreetings,
24
+ } from "../runtime/routes/workspace-greetings.js";
25
+
26
+ afterEach(() => {
27
+ for (const key of Object.keys(workspaceFiles)) {
28
+ delete workspaceFiles[key];
29
+ }
30
+ });
31
+
32
+ describe("parseGreetingsSection", () => {
33
+ test("parses bullet list from ## Greetings section", () => {
34
+ const content = [
35
+ "# Soul",
36
+ "",
37
+ "## Greetings",
38
+ "- Hey there, friend!",
39
+ "- What's on your mind?",
40
+ "- Ready to roll!",
41
+ "",
42
+ "## Other Section",
43
+ "Some other content.",
44
+ ].join("\n");
45
+
46
+ expect(parseGreetingsSection(content)).toEqual([
47
+ "Hey there, friend!",
48
+ "What's on your mind?",
49
+ "Ready to roll!",
50
+ ]);
51
+ });
52
+
53
+ test("handles asterisk bullets", () => {
54
+ const content = ["## Greetings", "* Hello!", "* Hi there."].join("\n");
55
+
56
+ expect(parseGreetingsSection(content)).toEqual(["Hello!", "Hi there."]);
57
+ });
58
+
59
+ test("handles plus and numbered bullets", () => {
60
+ const content = [
61
+ "## Greetings",
62
+ "+ Welcome back.",
63
+ "1. Ready when you are.",
64
+ "2) What are we building today?",
65
+ ].join("\n");
66
+
67
+ expect(parseGreetingsSection(content)).toEqual([
68
+ "Welcome back.",
69
+ "Ready when you are.",
70
+ "What are we building today?",
71
+ ]);
72
+ });
73
+
74
+ test("only starts at a level-two Greetings heading", () => {
75
+ const content = [
76
+ "# Greetings",
77
+ "- Not the section contract.",
78
+ "",
79
+ "## Greetings",
80
+ "- The real greeting.",
81
+ ].join("\n");
82
+
83
+ expect(parseGreetingsSection(content)).toEqual(["The real greeting."]);
84
+ });
85
+
86
+ test("returns null when section is missing", () => {
87
+ const content = ["# Soul", "## Personality", "Be friendly."].join("\n");
88
+
89
+ expect(parseGreetingsSection(content)).toBeNull();
90
+ });
91
+
92
+ test("returns null when section is empty", () => {
93
+ const content = ["## Greetings", "", "## Next Section"].join("\n");
94
+
95
+ expect(parseGreetingsSection(content)).toBeNull();
96
+ });
97
+
98
+ test("ignores non-bullet lines in section", () => {
99
+ const content = [
100
+ "## Greetings",
101
+ "Some intro text that's not a bullet",
102
+ "- Actual greeting",
103
+ "Another non-bullet line",
104
+ "- Second greeting",
105
+ ].join("\n");
106
+
107
+ expect(parseGreetingsSection(content)).toEqual([
108
+ "Actual greeting",
109
+ "Second greeting",
110
+ ]);
111
+ });
112
+
113
+ test("allows nested headings and stops at the next same-or-higher heading", () => {
114
+ const content = [
115
+ "## Greetings",
116
+ "- First",
117
+ "- Second",
118
+ "### Sub-heading",
119
+ "- Third",
120
+ "## Other Section",
121
+ "- Should not appear",
122
+ ].join("\n");
123
+
124
+ expect(parseGreetingsSection(content)).toEqual([
125
+ "First",
126
+ "Second",
127
+ "Third",
128
+ ]);
129
+ });
130
+ });
131
+
132
+ describe("readWorkspaceGreetings", () => {
133
+ test("reads greetings from SOUL.md", () => {
134
+ workspaceFiles["SOUL.md"] = [
135
+ "# Soul",
136
+ "## Greetings",
137
+ "- Hey!",
138
+ "- What's up?",
139
+ ].join("\n");
140
+
141
+ expect(readWorkspaceGreetings()).toEqual(["Hey!", "What's up?"]);
142
+ });
143
+
144
+ test("returns null when SOUL.md has no greetings section", () => {
145
+ workspaceFiles["SOUL.md"] = "# Soul\nBe friendly.";
146
+ expect(readWorkspaceGreetings()).toBeNull();
147
+ });
148
+
149
+ test("returns null when SOUL.md does not exist", () => {
150
+ expect(readWorkspaceGreetings()).toBeNull();
151
+ });
152
+ });
package/src/agent/loop.ts CHANGED
@@ -761,8 +761,10 @@ export class AgentLoop {
761
761
  /**
762
762
  * Compact the running history in place when the budget gate trips.
763
763
  *
764
- * Calls the default compaction plugin on the stripped history, then
765
- * re-applies injections via the supplied hooks. When `overflowSignal` is
764
+ * Calls the default compaction plugin, then re-applies injections via the
765
+ * supplied hooks. The budget path summarizes the injected `history` (so the
766
+ * summary call reuses the agent's warm prefix cache); overflow recovery runs
767
+ * on the injection-stripped history. When `overflowSignal` is
766
768
  * supplied the plugin routes through the manager's reduction ladder (which
767
769
  * advances one rung per call and reports `exhausted` / `autoCompressApplied`
768
770
  * / `injectionMode`); otherwise it runs ordinary forced compaction. Returns
@@ -795,8 +797,11 @@ export class AgentLoop {
795
797
  startedAt,
796
798
  messages: history,
797
799
  });
798
- // Strip runtime injections so the compactor summarizes the raw persistent
799
- // messages.
800
+ // Injection-stripped history for overflow recovery's reduction ladder and
801
+ // the post-compaction re-injection base below. The budget-path summarizer
802
+ // instead runs on the injected `history` so its prompt prefix matches the
803
+ // agent's warm prefix cache and the summary call is a cache read, not a
804
+ // fresh cache write.
800
805
  const rawHistory = stripInjectionsForCompaction(history);
801
806
  // Record the history-stripped marker right after stripping, before the
802
807
  // pipeline runs.
@@ -812,7 +817,7 @@ export class AgentLoop {
812
817
  // routes the request through the reduction ladder when present.
813
818
  const compactResult = await defaultCompact({
814
819
  conversationId: this.conversationId,
815
- messages: rawHistory,
820
+ messages: overflowSignal != null ? rawHistory : history,
816
821
  signal,
817
822
  force: true,
818
823
  actorTrustClass: trust.trustClass,
@@ -1434,6 +1439,7 @@ export class AgentLoop {
1434
1439
  conversationId: this.conversationId,
1435
1440
  callSite: callSite ?? null,
1436
1441
  systemPrompt: providerOptions.systemPrompt ?? null,
1442
+ modelProfile: effectiveOverrideProfile ?? null,
1437
1443
  deferAssistantOutput: false,
1438
1444
  logger: rlog,
1439
1445
  };
@@ -1443,6 +1449,20 @@ export class AgentLoop {
1443
1449
  );
1444
1450
  providerOptions.systemPrompt =
1445
1451
  finalPreModelCtx.systemPrompt ?? undefined;
1452
+ // Route this call to the hook's chosen inference profile. The
1453
+ // resolver layers `llm.profiles[overrideProfile]` at the top of
1454
+ // precedence for the user-facing call, so a model router can pick
1455
+ // the profile per message; clearing it drops any seeded override.
1456
+ const hookModelProfile = finalPreModelCtx.modelProfile?.trim();
1457
+ if (hookModelProfile) {
1458
+ providerConfig.overrideProfile = hookModelProfile;
1459
+ if (forceOverrideProfile) {
1460
+ providerConfig.forceOverrideProfile = true;
1461
+ }
1462
+ } else {
1463
+ delete providerConfig.overrideProfile;
1464
+ delete providerConfig.forceOverrideProfile;
1465
+ }
1446
1466
  // The hook owns the policy (it sees `callSite`/conversation and
1447
1467
  // self-gates); the loop honors whatever it decides.
1448
1468
  deferAssistantOutput = finalPreModelCtx.deferAssistantOutput;
package/src/api/README.md CHANGED
@@ -7,7 +7,7 @@ the wire contracts the assistant exposes: schemas, types, and pure helpers.
7
7
  Internal assistant code imports the files in this directory via relative paths
8
8
  (e.g. `../../api/events/open-url.js`). External consumers import the
9
9
  materialized npm-style package `@vellumai/assistant-api`, regenerated into
10
- `apps/web/node_modules/` by `apps/web/scripts/postinstall.ts`.
10
+ `clients/web/node_modules/` by `clients/web/scripts/postinstall.ts`.
11
11
 
12
12
  ## Architecture
13
13
 
@@ -72,7 +72,7 @@ a migrated event is the import and the union membership.
72
72
 
73
73
  ### 3. Cut over web consumers
74
74
 
75
- `apps/web/src/domains/chat/api/event-types.ts` no longer needs to list the
75
+ `clients/web/src/domains/chat/api/event-types.ts` no longer needs to list the
76
76
  migrated event in its `AssistantEvent` union — `APIAssistantEvent` covers it.
77
77
  Drop the per-event member, leaving the union to peel off legacy entries one
78
78
  at a time as each event migrates.
@@ -85,7 +85,7 @@ import them straight from the canonical package.
85
85
  ### 4. Delete the legacy parser cases
86
86
 
87
87
  Remove the matching `case "my_event":` blocks from
88
- `apps/web/src/domains/chat/api/event-parser.ts`. Any per-event helper
88
+ `clients/web/src/domains/chat/api/event-parser.ts`. Any per-event helper
89
89
  (`parseFooBase`, etc.) goes with them.
90
90
 
91
91
  ### 5. Tests
@@ -110,15 +110,15 @@ Handler-level tests in the consuming domain modules typically need no change
110
110
  Run before push, in order:
111
111
 
112
112
  ```bash
113
- # In apps/web — regenerate the @vellumai/assistant-api bundle
113
+ # In clients/web — regenerate the @vellumai/assistant-api bundle
114
114
  bun run scripts/postinstall.ts
115
115
 
116
116
  # Type-check both packages
117
117
  ( cd assistant && bunx tsc --noEmit )
118
- ( cd apps/web && bunx tsc --noEmit )
118
+ ( cd clients/web && bunx tsc --noEmit )
119
119
 
120
120
  # Targeted tests
121
- ( cd apps/web && bun test src/domains/chat/api/event-parser.test.ts )
121
+ ( cd clients/web && bun test src/domains/chat/api/event-parser.test.ts )
122
122
 
123
123
  # Lint + format the touched files
124
124
  bunx eslint <files>
@@ -390,10 +390,8 @@ export const ConversationMessageSchema = z.object({
390
390
  /**
391
391
  * @deprecated Superseded by `contentBlocks`. Flat plain-text body (joined
392
392
  * text segments). Redundant with `textSegments`/`contentOrder` for clients
393
- * that render from the positional arrays (web, CLI), but the legacy Swift
394
- * macOS client reads `content` directly and drops any history row missing it
395
- * (its `HistoryReconstructionService` skips rows with empty text). The
396
- * serializer always emits it — do not remove without updating that client.
393
+ * that render from the positional arrays (web, CLI). The serializer always
394
+ * emits it — do not remove without auditing clients that read it directly.
397
395
  */
398
396
  content: z
399
397
  .string()
@@ -12,10 +12,6 @@
12
12
  * generator, and the web/CLI clients all derive from one source and cannot
13
13
  * drift.
14
14
  *
15
- * A structurally compatible Swift mirror lives at
16
- * `clients/shared/Network/FeedItem.swift`. Any change to the feed-item
17
- * shape must be mirrored there.
18
- *
19
15
  * Canonical wire-contract source. Assistant code imports the types
20
16
  * directly from this file via relative paths; external consumers
21
17
  * (web client, gateway, evals) import via `@vellumai/assistant-api`.
@@ -373,7 +373,7 @@ const accessRequestResolver: GuardianRequestResolver = {
373
373
  const requesterContactResult = requesterExternalUserId
374
374
  ? findContactChannel({
375
375
  channelType: channel,
376
- externalUserId: requesterExternalUserId,
376
+ address: requesterExternalUserId,
377
377
  })
378
378
  : null;
379
379
  const requesterDisplayName =
@@ -382,7 +382,7 @@ const accessRequestResolver: GuardianRequestResolver = {
382
382
  const decidedByContactResult = decidedByExternalUserId
383
383
  ? findContactChannel({
384
384
  channelType: channel,
385
- externalUserId: decidedByExternalUserId,
385
+ address: decidedByExternalUserId,
386
386
  })
387
387
  : null;
388
388
  const decidedByDisplayName =
@@ -249,7 +249,7 @@ export function emitAccessRequestCallbackHandoff(
249
249
  try {
250
250
  const contactResult = findContactChannel({
251
251
  channelType: "phone",
252
- externalUserId: fromNumber,
252
+ address: fromNumber,
253
253
  externalChatId: fromNumber,
254
254
  });
255
255
  if (
@@ -553,8 +553,8 @@ export async function startVoiceTurn(
553
553
  } else if (msg.type === "secret_request") {
554
554
  if (usesLocalInteractiveApprovals) {
555
555
  // Local live voice runs alongside the desktop client, which has a
556
- // secret-entry UI (SecretPromptManager). Forward the broadcast and
557
- // let the prompter's existing registration handle the response.
556
+ // secret-entry UI. Forward the broadcast and let the prompter's
557
+ // existing registration handle the response.
558
558
  broadcastMessage(msg);
559
559
  return;
560
560
  }
@@ -10,6 +10,11 @@
10
10
  import type { Command } from "commander";
11
11
 
12
12
  import { confirmPrompt } from "../lib/confirm-prompt.js";
13
+ import {
14
+ diffPlugin,
15
+ type PluginDiffResult,
16
+ PluginDiffUnavailableError,
17
+ } from "../lib/diff-plugin.js";
13
18
  import {
14
19
  inspectPlugin,
15
20
  type PluginInspection,
@@ -35,8 +40,13 @@ import {
35
40
  uninstallPlugin,
36
41
  } from "../lib/uninstall-plugin.js";
37
42
  import {
43
+ DEFAULT_PLUGIN_UPGRADE_STRATEGY,
44
+ PLUGIN_UPGRADE_STRATEGIES,
45
+ PluginMergeBaselineError,
38
46
  PluginNotUpgradableError,
39
47
  type PluginUpgradeResult,
48
+ type PluginUpgradeStrategy,
49
+ PluginUpgradeStrategyUnsupportedError,
40
50
  upgradePlugin,
41
51
  } from "../lib/upgrade-plugin.js";
42
52
  import { getCliLogger } from "../logger.js";
@@ -60,8 +70,12 @@ Examples:
60
70
  $ assistant plugins list --json
61
71
  $ assistant plugins inspect example
62
72
  $ assistant plugins inspect example --json
73
+ $ assistant plugins diff example
74
+ $ assistant plugins diff example --json
63
75
  $ assistant plugins upgrade example
64
76
  $ assistant plugins upgrade example --dry-run
77
+ $ assistant plugins upgrade example --strategy ours
78
+ $ assistant plugins upgrade example --strategy theirs
65
79
  $ assistant plugins search example
66
80
  $ assistant plugins search "^example"
67
81
  $ assistant plugins search example --json
@@ -212,6 +226,73 @@ Examples:
212
226
  }
213
227
  });
214
228
 
229
+ plugins
230
+ .command("diff <name>")
231
+ .description(
232
+ "Show a unified diff of local edits to an installed plugin against the commit it was installed at",
233
+ )
234
+ .option(
235
+ "--json",
236
+ "Emit the machine-readable diff result as JSON (files: { path, status, diff, binary, reconstructed }[]) instead of a unified diff",
237
+ )
238
+ .addHelpText(
239
+ "after",
240
+ `
241
+ Arguments:
242
+ name Install name (kebab-case directory under the workspace plugins dir);
243
+ run 'assistant plugins list' to see installed names.
244
+
245
+ The baseline is the exact commit the plugin was installed at (recorded in its
246
+ install-meta.json), re-materialized through the install pipeline — so an
247
+ install-time adapter transform never reads as a local change. To compare
248
+ against the marketplace's current pin instead, use 'plugins upgrade --dry-run'.
249
+
250
+ Examples:
251
+ $ assistant plugins diff example
252
+ $ assistant plugins diff example --json`,
253
+ )
254
+ .action(async (name: string, opts: { json?: boolean }) => {
255
+ try {
256
+ const result = await diffPlugin(
257
+ { name },
258
+ { fetch: globalThis.fetch.bind(globalThis) },
259
+ );
260
+
261
+ if (opts.json) {
262
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
263
+ return;
264
+ }
265
+
266
+ // Logged after the JSON early-return: the CLI logger writes
267
+ // info to stdout, which would otherwise corrupt --json output.
268
+ log.info(
269
+ {
270
+ name: result.name,
271
+ clean: result.clean,
272
+ files: result.files.length,
273
+ },
274
+ "plugin diff",
275
+ );
276
+
277
+ for (const line of formatDiff(result)) {
278
+ console.log(line);
279
+ }
280
+ } catch (err) {
281
+ if (
282
+ err instanceof PluginNotInstalledError ||
283
+ err instanceof PluginDiffUnavailableError ||
284
+ err instanceof InvalidPluginNameError
285
+ ) {
286
+ console.error(err.message);
287
+ process.exitCode = 1;
288
+ return;
289
+ }
290
+ const message = err instanceof Error ? err.message : String(err);
291
+ console.error(`Plugin diff failed: ${message}`);
292
+ process.exitCode = 1;
293
+ }
294
+ });
295
+
215
296
  plugins
216
297
  .command("search <query>")
217
298
  .description(
@@ -327,12 +408,36 @@ Examples:
327
408
  "--dry-run",
328
409
  "Show what would change without modifying the install",
329
410
  )
411
+ .option(
412
+ "--strategy <strategy>",
413
+ `How to reconcile local edits with the pin: ${PLUGIN_UPGRADE_STRATEGIES.join(", ")} (default: ${DEFAULT_PLUGIN_UPGRADE_STRATEGY})`,
414
+ )
330
415
  .option("--json", "Emit machine-readable JSON instead of a summary")
331
416
  .action(
332
- async (name: string, opts: { dryRun?: boolean; json?: boolean }) => {
417
+ async (
418
+ name: string,
419
+ opts: { dryRun?: boolean; strategy?: string; json?: boolean },
420
+ ) => {
421
+ const strategy = opts.strategy;
422
+ if (
423
+ strategy !== undefined &&
424
+ !(PLUGIN_UPGRADE_STRATEGIES as readonly string[]).includes(
425
+ strategy,
426
+ )
427
+ ) {
428
+ console.error(
429
+ `Invalid --strategy "${strategy}". Expected one of: ${PLUGIN_UPGRADE_STRATEGIES.join(", ")}.`,
430
+ );
431
+ process.exitCode = 1;
432
+ return;
433
+ }
333
434
  try {
334
435
  const result = await upgradePlugin(
335
- { name, dryRun: opts.dryRun },
436
+ {
437
+ name,
438
+ dryRun: opts.dryRun,
439
+ strategy: strategy as PluginUpgradeStrategy | undefined,
440
+ },
336
441
  { fetch: globalThis.fetch.bind(globalThis) },
337
442
  );
338
443
 
@@ -360,6 +465,8 @@ Examples:
360
465
  if (
361
466
  err instanceof PluginNotInstalledError ||
362
467
  err instanceof PluginNotUpgradableError ||
468
+ err instanceof PluginUpgradeStrategyUnsupportedError ||
469
+ err instanceof PluginMergeBaselineError ||
363
470
  err instanceof InvalidPluginNameError
364
471
  ) {
365
472
  console.error(err.message);
@@ -521,14 +628,48 @@ function formatUpgrade(result: PluginUpgradeResult): string[] {
521
628
  result.fileCount === null
522
629
  ? ""
523
630
  : `(${result.fileCount} file${result.fileCount === 1 ? "" : "s"}) `;
631
+ const mergeNote =
632
+ result.strategy === "ours" || result.strategy === "theirs"
633
+ ? `Local edits were merged (--strategy ${result.strategy}).`
634
+ : null;
524
635
  const lines = [
525
636
  `Upgraded "${name}" ${move}`,
526
637
  "",
527
638
  `${count}→ ${result.target}`,
528
639
  "Restart the assistant to pick up the upgrade.",
529
640
  ];
641
+ if (mergeNote) lines.push(mergeNote);
530
642
  if (provenanceNote) lines.push(provenanceNote);
531
643
  return lines;
532
644
  }
533
645
  }
534
646
  }
647
+
648
+ /**
649
+ * Render a diff result: a one-line "no local changes" when clean, otherwise a
650
+ * header naming the install-commit baseline followed by each file's unified
651
+ * diff (blank-line separated, mirroring how `git diff` stacks file patches).
652
+ */
653
+ function formatDiff(result: PluginDiffResult): string[] {
654
+ const baseline = `${formatTimestamp(result.committedAt)} (${shortSha(result.commit)})`;
655
+ if (result.clean) {
656
+ return [
657
+ `"${result.name}" has no local changes (matches install commit ${baseline}).`,
658
+ ];
659
+ }
660
+ const count = result.files.length;
661
+ const lines = [
662
+ `"${result.name}" — ${count} file${count === 1 ? "" : "s"} changed vs install commit ${baseline}`,
663
+ "",
664
+ ];
665
+ for (const file of result.files) {
666
+ // A non-reconstructed baseline carries an explanatory marker, not a patch
667
+ // with `a/`–`b/` headers, so name the file it refers to.
668
+ if (!file.reconstructed) {
669
+ lines.push(`${file.path}: ${file.diff.trimEnd()}`, "");
670
+ continue;
671
+ }
672
+ lines.push(file.diff.trimEnd(), "");
673
+ }
674
+ return lines;
675
+ }