@vellumai/assistant 0.8.9-staging.2 → 0.8.9-staging.4

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 (196) hide show
  1. package/docs/activation-funnel-telemetry.md +310 -0
  2. package/openapi.yaml +16 -115
  3. package/package.json +1 -1
  4. package/src/__tests__/activation-early-marking.test.ts +120 -0
  5. package/src/__tests__/agent-loop-output-hooks.test.ts +13 -13
  6. package/src/__tests__/anthropic-provider.test.ts +23 -8
  7. package/src/__tests__/approval-cascade.test.ts +1 -1
  8. package/src/__tests__/compaction-direct.test.ts +32 -18
  9. package/src/__tests__/compaction-events.test.ts +2 -2
  10. package/src/__tests__/compaction.benchmark.test.ts +1 -1
  11. package/src/__tests__/context-overflow-reducer.test.ts +5 -5
  12. package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
  13. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  14. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  15. package/src/__tests__/conversation-error.test.ts +15 -1
  16. package/src/__tests__/conversation-history-web-search.test.ts +5 -0
  17. package/src/__tests__/conversation-media-retry.test.ts +1 -1
  18. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +40 -0
  19. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  20. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  21. package/src/__tests__/conversation-queue.test.ts +1 -1
  22. package/src/__tests__/conversation-runtime-assembly.test.ts +71 -0
  23. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  24. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  25. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  26. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
  27. package/src/__tests__/conversation-surfaces-app-control.test.ts +44 -0
  28. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
  29. package/src/__tests__/conversation-undo.test.ts +2 -2
  30. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  31. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  32. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  33. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  34. package/src/__tests__/cu-unified-flow.test.ts +36 -0
  35. package/src/__tests__/history-repair-hook.test.ts +2 -0
  36. package/src/__tests__/llm-resolver.test.ts +73 -0
  37. package/src/__tests__/memory-retrieval-hook.test.ts +1 -2
  38. package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
  39. package/src/__tests__/persist-unsendable-image.test.ts +97 -1
  40. package/src/__tests__/plugin-external-api.test.ts +68 -0
  41. package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
  42. package/src/__tests__/published-app-updater.test.ts +138 -0
  43. package/src/__tests__/skill-feature-flags-integration.test.ts +5 -7
  44. package/src/__tests__/title-generate-hook.test.ts +2 -0
  45. package/src/__tests__/web-fetch.test.ts +45 -0
  46. package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
  47. package/src/acp/resolve-agent.test.ts +0 -56
  48. package/src/acp/resolve-agent.ts +10 -38
  49. package/src/agent/loop.ts +13 -27
  50. package/src/api/responses/memory-v3-selection-log.ts +19 -10
  51. package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
  52. package/src/cli/commands/memory-v3.ts +57 -199
  53. package/src/cli/lib/__tests__/install-from-github.test.ts +232 -29
  54. package/src/cli/lib/__tests__/plugin-details.test.ts +28 -19
  55. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +57 -7
  56. package/src/cli/lib/__tests__/search-plugins.test.ts +17 -10
  57. package/src/cli/lib/install-from-github.ts +258 -41
  58. package/src/cli/lib/plugin-details.ts +20 -13
  59. package/src/cli/lib/plugin-marketplace.ts +23 -5
  60. package/src/cli/lib/search-plugins.ts +14 -8
  61. package/src/config/acp-defaults.ts +3 -3
  62. package/src/config/acp-schema.ts +1 -7
  63. package/src/config/bundled-skills/acp/SKILL.md +4 -17
  64. package/src/config/bundled-skills/acp/TOOLS.json +2 -2
  65. package/src/config/call-site-defaults.ts +0 -1
  66. package/src/config/feature-flag-registry.json +3 -18
  67. package/src/config/llm-resolver.ts +39 -7
  68. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
  69. package/src/config/schemas/call-site-catalog.ts +0 -7
  70. package/src/config/schemas/llm.ts +17 -1
  71. package/src/config/schemas/memory-v3.ts +58 -8
  72. package/src/config/seed-inference-profiles.ts +18 -0
  73. package/src/context/post-turn-tool-result-truncation.ts +39 -1
  74. package/src/daemon/conversation-agent-loop-handlers.ts +8 -1
  75. package/src/daemon/conversation-agent-loop.ts +87 -95
  76. package/src/daemon/conversation-error.ts +31 -4
  77. package/src/daemon/conversation-history.ts +1 -1
  78. package/src/daemon/conversation-media-retry.ts +19 -6
  79. package/src/daemon/conversation-messaging.ts +17 -0
  80. package/src/daemon/conversation-process.ts +14 -5
  81. package/src/daemon/conversation-queue-manager.ts +8 -0
  82. package/src/daemon/conversation-runtime-assembly.ts +37 -1
  83. package/src/daemon/conversation-surfaces.ts +141 -3
  84. package/src/daemon/conversation.ts +48 -13
  85. package/src/daemon/external-plugins-bootstrap.ts +8 -3
  86. package/src/daemon/persist-unsendable-image.ts +62 -25
  87. package/src/daemon/process-message.ts +1 -1
  88. package/src/daemon/tool-side-effects.ts +15 -0
  89. package/src/memory/__tests__/activation-session-store.test.ts +41 -0
  90. package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
  91. package/src/memory/activation-session-store.ts +43 -0
  92. package/src/memory/db-init.ts +4 -0
  93. package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
  94. package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
  95. package/src/memory/migrations/index.ts +2 -0
  96. package/src/memory/onboarding-events-store.ts +66 -18
  97. package/src/memory/schema/infrastructure.ts +13 -0
  98. package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
  99. package/src/memory/v2/__tests__/page-store.test.ts +22 -0
  100. package/src/memory/v2/consolidation-job.ts +2 -72
  101. package/src/memory/v2/types.ts +5 -0
  102. package/src/messaging/providers/telegram-bot/api.ts +14 -5
  103. package/src/notifications/adapters/telegram.ts +7 -1
  104. package/src/plugin-api/constants.ts +2 -2
  105. package/src/plugin-api/index.ts +2 -2
  106. package/src/plugin-api/types.ts +19 -5
  107. package/src/plugins/defaults/compaction/compact.ts +24 -15
  108. package/src/plugins/defaults/compaction/context-overflow-reducer.ts +4 -4
  109. package/src/plugins/defaults/compaction/manager-store.ts +1 -1
  110. package/src/{context → plugins/defaults/compaction}/window-manager.ts +68 -12
  111. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +12 -18
  112. package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +19 -66
  113. package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
  114. package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
  115. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +139 -129
  116. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +332 -164
  117. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +516 -293
  118. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
  119. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +51 -21
  120. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
  121. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
  122. package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
  123. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +66 -11
  124. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
  125. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +271 -110
  126. package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +0 -22
  127. package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +26 -62
  128. package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
  129. package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
  130. package/src/plugins/defaults/memory-v3-shadow/injector.ts +3 -2
  131. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +415 -181
  132. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +196 -56
  133. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +39 -2
  134. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
  135. package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +15 -7
  136. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
  137. package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
  138. package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
  139. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +75 -3
  140. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +111 -78
  141. package/src/plugins/defaults/memory-v3-shadow/types.ts +52 -19
  142. package/src/plugins/defaults/memory-v3-shadow/working-set.ts +4 -1
  143. package/src/plugins/external-api.ts +114 -0
  144. package/src/prompts/system-prompt.ts +61 -10
  145. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +37 -2
  146. package/src/providers/anthropic/client.ts +9 -10
  147. package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
  148. package/src/providers/inference/logit-bias.ts +55 -0
  149. package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
  150. package/src/providers/openai/chat-completions-provider.ts +44 -1
  151. package/src/providers/retry.ts +22 -0
  152. package/src/providers/types.ts +6 -0
  153. package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
  154. package/src/runtime/routes/acp-routes.test.ts +3 -20
  155. package/src/runtime/routes/app-management-routes.ts +3 -0
  156. package/src/runtime/routes/conversation-routes.ts +2 -0
  157. package/src/runtime/routes/memory-v3-routes.ts +64 -314
  158. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
  159. package/src/runtime/routes/stt-routes.ts +45 -12
  160. package/src/runtime/routes/workspace-routes.ts +50 -15
  161. package/src/services/published-app-updater.ts +30 -8
  162. package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
  163. package/src/telemetry/activation-funnel.ts +167 -0
  164. package/src/telemetry/types.ts +13 -0
  165. package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
  166. package/src/telemetry/usage-telemetry-reporter.ts +26 -1
  167. package/src/tools/acp/list-agents.test.ts +2 -18
  168. package/src/tools/acp/list-agents.ts +3 -15
  169. package/src/tools/acp/spawn.test.ts +0 -10
  170. package/src/tools/browser/browser-execution.ts +12 -2
  171. package/src/tools/network/web-fetch.ts +65 -24
  172. package/src/tools/skills/load.ts +1 -1
  173. package/src/tools/ui-surface/definitions.ts +7 -0
  174. package/src/acp/feature-gate.test.ts +0 -48
  175. package/src/acp/feature-gate.ts +0 -34
  176. package/src/plugins/defaults/memory-v3-shadow/__tests__/assign.test.ts +0 -242
  177. package/src/plugins/defaults/memory-v3-shadow/__tests__/core.test.ts +0 -39
  178. package/src/plugins/defaults/memory-v3-shadow/__tests__/health.test.ts +0 -219
  179. package/src/plugins/defaults/memory-v3-shadow/__tests__/needle.test.ts +0 -107
  180. package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +0 -13
  181. package/src/plugins/defaults/memory-v3-shadow/__tests__/reconcile.test.ts +0 -274
  182. package/src/plugins/defaults/memory-v3-shadow/__tests__/router.test.ts +0 -337
  183. package/src/plugins/defaults/memory-v3-shadow/__tests__/selector.test.ts +0 -470
  184. package/src/plugins/defaults/memory-v3-shadow/__tests__/snapshot.test.ts +0 -168
  185. package/src/plugins/defaults/memory-v3-shadow/__tests__/tree.test.ts +0 -192
  186. package/src/plugins/defaults/memory-v3-shadow/assign.ts +0 -272
  187. package/src/plugins/defaults/memory-v3-shadow/core.ts +0 -26
  188. package/src/plugins/defaults/memory-v3-shadow/health.ts +0 -0
  189. package/src/plugins/defaults/memory-v3-shadow/needle.ts +0 -115
  190. package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +0 -26
  191. package/src/plugins/defaults/memory-v3-shadow/reconcile.ts +0 -527
  192. package/src/plugins/defaults/memory-v3-shadow/router.ts +0 -190
  193. package/src/plugins/defaults/memory-v3-shadow/selector.ts +0 -226
  194. package/src/plugins/defaults/memory-v3-shadow/snapshot.ts +0 -209
  195. package/src/plugins/defaults/memory-v3-shadow/tree.ts +0 -174
  196. package/src/util/map-limit.ts +0 -27
@@ -12,6 +12,7 @@ import {
12
12
  TARGET_CHARS,
13
13
  THRESHOLD_CHARS,
14
14
  TOOL_RESULT_DIR,
15
+ TRUNCATION_EXEMPT_TOOLS,
15
16
  TRUNCATION_MARKER,
16
17
  } from "../context/post-turn-tool-result-truncation.js";
17
18
  import type { ContentBlock, Message } from "../providers/types.js";
@@ -151,6 +152,74 @@ describe("postTurnTruncateToolResults", () => {
151
152
  expect(stub).toContain(filePath);
152
153
  });
153
154
 
155
+ test("skill_load result above threshold is NOT truncated (durable instructions exempt)", () => {
156
+ // Regression for JARVIS-1000: a hosted assistant lost its app-builder skill
157
+ // workflow when the large skill_load result was middle-truncated between the
158
+ // turn that loaded the skill and the turn that used it, then fell back to a
159
+ // local-dev (vite/localhost) build path.
160
+ const toolUseId = "tool_skill_load";
161
+ const skillBody = "S".repeat(THRESHOLD_CHARS + 5_000);
162
+ const messages: Message[] = [
163
+ {
164
+ role: "assistant",
165
+ content: [
166
+ {
167
+ type: "tool_use" as const,
168
+ id: toolUseId,
169
+ name: "skill_load",
170
+ input: { skill: "app-builder" },
171
+ },
172
+ ],
173
+ },
174
+ { role: "user", content: [makeToolResult(skillBody, toolUseId)] },
175
+ ];
176
+
177
+ const { messages: result, truncatedCount } =
178
+ postTurnTruncateToolResults(messages, { conversationDir: convDir });
179
+
180
+ expect(truncatedCount).toBe(0);
181
+ expect(result).toBe(messages); // same reference — no copy
182
+ expect(existsSync(join(convDir, TOOL_RESULT_DIR))).toBe(false);
183
+
184
+ const block = result[1].content[0] as {
185
+ type: "tool_result";
186
+ content: string;
187
+ };
188
+ expect(block.content).toBe(skillBody);
189
+ expect(TRUNCATION_EXEMPT_TOOLS.has("skill_load")).toBe(true);
190
+ });
191
+
192
+ test("non-exempt tool result above threshold is still truncated when paired with a tool_use", () => {
193
+ // Control for the exemption: same shape as the skill_load case, but a tool
194
+ // that is NOT exempt must still be truncated.
195
+ const toolUseId = "tool_bash_1";
196
+ const longContent = "B".repeat(THRESHOLD_CHARS + 5_000);
197
+ const messages: Message[] = [
198
+ {
199
+ role: "assistant",
200
+ content: [
201
+ {
202
+ type: "tool_use" as const,
203
+ id: toolUseId,
204
+ name: "bash",
205
+ input: { command: "cat big.log" },
206
+ },
207
+ ],
208
+ },
209
+ { role: "user", content: [makeToolResult(longContent, toolUseId)] },
210
+ ];
211
+
212
+ const { messages: result, truncatedCount } =
213
+ postTurnTruncateToolResults(messages, { conversationDir: convDir });
214
+
215
+ expect(truncatedCount).toBe(1);
216
+ const block = result[1].content[0] as {
217
+ type: "tool_result";
218
+ content: string;
219
+ };
220
+ expect(block.content).toContain(TRUNCATION_MARKER);
221
+ });
222
+
154
223
  test("file path is deterministic for the same toolUseId", () => {
155
224
  const id = "tool_use_deterministic";
156
225
  const path1 = getToolResultFilePath("/some/dir", id);
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Tests that the auto-redeploy path deploys the app's *effective* HTML
3
+ * (dist/index.html for multifile apps) rather than the empty `htmlDefinition`.
4
+ */
5
+
6
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
7
+
8
+ import type { AppDefinition } from "../memory/app-store.js";
9
+
10
+ // ── Mocks ───────────────────────────────────────────────────────────────
11
+
12
+ let mockApp: AppDefinition | null = null;
13
+ let mockIsMultifile = false;
14
+ let mockEffectiveHtml = "";
15
+ // A directory that does not exist on disk, so the multifile dist-existence
16
+ // guard reads `false` from the real fs without mocking node:fs (which would
17
+ // leak across test files).
18
+ let mockAppDir = "/tmp/__vellum_test_nonexistent__/app-1";
19
+
20
+ mock.module("../memory/app-store.js", () => ({
21
+ getApp: () => mockApp,
22
+ getAppDirPath: () => mockAppDir,
23
+ isMultifileApp: () => mockIsMultifile,
24
+ resolveEffectiveAppHtml: () => mockEffectiveHtml,
25
+ }));
26
+
27
+ let mockPublishedPage: {
28
+ id: string;
29
+ projectSlug?: string;
30
+ htmlHash: string;
31
+ } | null = null;
32
+ const updatePublishedPageSpy = mock(() => {});
33
+
34
+ mock.module("../memory/published-pages-store.js", () => ({
35
+ getActivePublishedPageByAppId: () => mockPublishedPage,
36
+ updatePublishedPage: updatePublishedPageSpy,
37
+ }));
38
+
39
+ const deploySpy = mock(async (_args: { html: string; name: string }) => ({
40
+ deploymentId: "dep-1",
41
+ url: "https://example.vercel.app",
42
+ }));
43
+
44
+ mock.module("../services/vercel-deploy.js", () => ({
45
+ deployHtmlToVercel: deploySpy,
46
+ }));
47
+
48
+ // credentialBroker.serverUse invokes execute(token) and reports success.
49
+ mock.module("../tools/credentials/broker.js", () => ({
50
+ credentialBroker: {
51
+ serverUse: async ({
52
+ execute,
53
+ }: {
54
+ execute: (token: string) => Promise<unknown>;
55
+ }) => {
56
+ await execute("test-token");
57
+ return { success: true };
58
+ },
59
+ },
60
+ }));
61
+
62
+ const { updatePublishedAppDeployment } =
63
+ await import("../services/published-app-updater.js");
64
+
65
+ function makeApp(overrides: Partial<AppDefinition> = {}): AppDefinition {
66
+ return {
67
+ id: "app-1",
68
+ name: "App",
69
+ schemaJson: "{}",
70
+ htmlDefinition: "",
71
+ createdAt: 1,
72
+ updatedAt: 2,
73
+ ...overrides,
74
+ };
75
+ }
76
+
77
+ // ── Tests ───────────────────────────────────────────────────────────────
78
+
79
+ describe("updatePublishedAppDeployment", () => {
80
+ beforeEach(() => {
81
+ mockApp = makeApp();
82
+ mockIsMultifile = false;
83
+ mockEffectiveHtml = "";
84
+ mockPublishedPage = { id: "pp-1", projectSlug: "slug", htmlHash: "old" };
85
+ mockAppDir = "/tmp/__vellum_test_nonexistent__/app-1";
86
+ deploySpy.mockClear();
87
+ updatePublishedPageSpy.mockClear();
88
+ });
89
+
90
+ test("deploys the resolved effective HTML, not the empty htmlDefinition", async () => {
91
+ // htmlDefinition is "" (as it is for every multifile app); the real
92
+ // content comes from resolveEffectiveAppHtml. isMultifile=false here keeps
93
+ // the dist guard out of the way so the assertion targets the html source.
94
+ mockApp = makeApp({ htmlDefinition: "" });
95
+ mockEffectiveHtml = "<html><body>real app</body></html>";
96
+
97
+ await updatePublishedAppDeployment("app-1");
98
+
99
+ expect(deploySpy).toHaveBeenCalledTimes(1);
100
+ expect(deploySpy.mock.calls[0][0].html).toBe(
101
+ "<html><body>real app</body></html>",
102
+ );
103
+ });
104
+
105
+ test("skips deploy when a multifile app has no compiled output", async () => {
106
+ mockIsMultifile = true;
107
+ mockApp = makeApp({ formatVersion: 2 });
108
+ mockEffectiveHtml = "<p>App compilation failed.</p>";
109
+ // mockAppDir does not exist → dist/index.html is absent.
110
+
111
+ await updatePublishedAppDeployment("app-1");
112
+
113
+ expect(deploySpy).not.toHaveBeenCalled();
114
+ });
115
+
116
+ test("skips deploy when content hash is unchanged", async () => {
117
+ const html = "<html>same</html>";
118
+ mockEffectiveHtml = html;
119
+ const { createHash } = await import("node:crypto");
120
+ mockPublishedPage = {
121
+ id: "pp-1",
122
+ htmlHash: createHash("sha256").update(html).digest("hex"),
123
+ };
124
+
125
+ await updatePublishedAppDeployment("app-1");
126
+
127
+ expect(deploySpy).not.toHaveBeenCalled();
128
+ });
129
+
130
+ test("skips deploy when the app has no active published page", async () => {
131
+ mockPublishedPage = null;
132
+ mockEffectiveHtml = "<html>x</html>";
133
+
134
+ await updatePublishedAppDeployment("app-1");
135
+
136
+ expect(deploySpy).not.toHaveBeenCalled();
137
+ });
138
+ });
@@ -233,10 +233,9 @@ describe("frontmatter feature-flag integration", () => {
233
233
  // ---------------------------------------------------------------------------
234
234
 
235
235
  describe("bundled acp skill discoverability", () => {
236
- test("acp skill resolves with the flag off and config.acp disabled (no frontmatter flag gate)", () => {
237
- // The ACP skill carries its own first-time-setup instructions, so it must
238
- // stay visible even when the acp flag and config.acp.enabled are both off.
239
- // Runtime enforcement happens in the ACP tools via isAcpEnabled instead.
236
+ test("acp skill resolves with no frontmatter flag gate", () => {
237
+ // The ACP skill carries its own first-time-setup instructions and is
238
+ // always discoverable: it has no frontmatter feature-flag gate.
240
239
  const skillMdPath = fileURLToPath(
241
240
  new URL("../config/bundled-skills/acp/SKILL.md", import.meta.url),
242
241
  );
@@ -247,10 +246,9 @@ describe("bundled acp skill discoverability", () => {
247
246
  expect(skill!.featureFlag).toBeUndefined();
248
247
  expect(skillFlagKey(skill!)).toBeUndefined();
249
248
 
250
- // acp flag at its registry default (off) and config.acp disabled.
251
249
  const config = makeConfig({
252
- acp: { enabled: false, maxConcurrentSessions: 4, agents: {} },
253
- } as Partial<AssistantConfig>);
250
+ acp: { maxConcurrentSessions: 4, agents: {} },
251
+ });
254
252
 
255
253
  const resolved = resolveSkillStates([skill!], config);
256
254
  expect(resolved.length).toBe(1);
@@ -76,6 +76,8 @@ function makeCtx(
76
76
  ];
77
77
  return {
78
78
  conversationId: "conv-1",
79
+ userMessageId: "msg-1",
80
+ requestId: "req-1",
79
81
  prompt: "first message",
80
82
  originalMessages: messages,
81
83
  latestMessages: messages,
@@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
5
5
  import {
6
6
  buildFetchResponseFromNodeResponse,
7
7
  executeWebFetch,
8
+ getUpstreamStatus,
8
9
  } from "../tools/network/web-fetch.js";
9
10
 
10
11
  describe("web_fetch tool", () => {
@@ -62,6 +63,50 @@ describe("web_fetch tool", () => {
62
63
  expect(await response.text()).toBe("");
63
64
  });
64
65
 
66
+ test("buildFetchResponseFromNodeResponse does not throw on non-standard status codes", async () => {
67
+ const stream = new PassThrough() as PassThrough & {
68
+ statusCode?: number;
69
+ statusMessage?: string;
70
+ headers: IncomingHttpHeaders;
71
+ };
72
+ stream.statusCode = 999;
73
+ stream.statusMessage = "Request Denied";
74
+ stream.headers = { "content-type": "text/html; charset=utf-8" };
75
+ stream.end("blocked by anti-bot gateway");
76
+
77
+ const response = buildFetchResponseFromNodeResponse(stream);
78
+ // The Response object clamps to a constructable code, but the real upstream
79
+ // status is preserved for reporting.
80
+ expect(response.status).toBe(502);
81
+ expect(getUpstreamStatus(response)).toBe(999);
82
+ expect(response.ok).toBe(false);
83
+ expect(await response.text()).toBe("blocked by anti-bot gateway");
84
+ });
85
+
86
+ test("surfaces a non-standard status as a tool error instead of crashing", async () => {
87
+ const result = await executeWithMockFetch(
88
+ { url: "https://www.linkedin.com/jobs/view/123" },
89
+ {
90
+ requestExecutor: async () => {
91
+ const stream = new PassThrough() as PassThrough & {
92
+ statusCode?: number;
93
+ statusMessage?: string;
94
+ headers: IncomingHttpHeaders;
95
+ };
96
+ stream.statusCode = 999;
97
+ stream.statusMessage = "Request Denied";
98
+ stream.headers = { "content-type": "text/html; charset=utf-8" };
99
+ stream.end("<html><body>blocked</body></html>");
100
+ return buildFetchResponseFromNodeResponse(stream);
101
+ },
102
+ },
103
+ );
104
+
105
+ expect(result.isError).toBe(true);
106
+ expect(result.content).toContain("HTTP 999");
107
+ expect(result.activityMetadata?.webFetch?.status).toBe(999);
108
+ });
109
+
65
110
  test("rejects missing url", async () => {
66
111
  const result = await executeWithMockFetch({});
67
112
  expect(result.isError).toBe(true);
@@ -20,13 +20,11 @@ import { mock } from "bun:test";
20
20
  import type { AcpAgentConfig } from "../../../config/acp-schema.js";
21
21
 
22
22
  export interface MockAcpConfig {
23
- enabled: boolean;
24
23
  maxConcurrentSessions: number;
25
24
  agents: Record<string, AcpAgentConfig>;
26
25
  }
27
26
 
28
27
  const DEFAULT_CONFIG: MockAcpConfig = {
29
- enabled: true,
30
28
  maxConcurrentSessions: 4,
31
29
  agents: {},
32
30
  };
@@ -1,25 +1,19 @@
1
1
  import { afterAll, beforeEach, describe, expect, test } from "bun:test";
2
2
 
3
- import { setOverridesForTesting } from "../__tests__/feature-flag-test-helpers.js";
4
3
  import { installAcpConfigStub } from "./__tests__/helpers/acp-config-stub.js";
5
4
  import { installWhichStub } from "./__tests__/helpers/which-stub.js";
6
- import { ACP_FLAG_KEY } from "./feature-gate.js";
7
5
 
8
6
  const config = await installAcpConfigStub();
9
7
  const which = installWhichStub();
10
8
 
11
9
  afterAll(() => {
12
10
  which.restore();
13
- setOverridesForTesting({});
14
11
  });
15
12
 
16
13
  const { resolveAcpAgent, listAcpAgents } = await import("./resolve-agent.js");
17
14
 
18
15
  beforeEach(() => {
19
16
  config.setConfig({});
20
- // Default: no flag overrides, so the `acp` flag falls back to its registry
21
- // default (false) and enablement comes from the config stub alone.
22
- setOverridesForTesting({});
23
17
  // Default: every command on PATH so binary preflight passes unless a test
24
18
  // explicitly says otherwise.
25
19
  which.setWhich((cmd) => `/usr/local/bin/${cmd}`);
@@ -30,32 +24,6 @@ beforeEach(() => {
30
24
  // ---------------------------------------------------------------------------
31
25
 
32
26
  describe("resolveAcpAgent", () => {
33
- test("returns acp_disabled when both the feature flag and config.acp.enabled are off", () => {
34
- config.setConfig({ enabled: false });
35
-
36
- const result = resolveAcpAgent("claude");
37
-
38
- expect(result.ok).toBe(false);
39
- if (result.ok) return;
40
- expect(result.reason).toBe("acp_disabled");
41
- if (result.reason !== "acp_disabled") return;
42
- expect(result.hint).toContain("ACP Coding Agents");
43
- expect(result.hint).toContain("feature flag");
44
- expect(result.hint).toContain("acp.enabled");
45
- expect(result.hint).toContain("config.json");
46
- });
47
-
48
- test("resolution proceeds when the acp feature flag is on and config.acp.enabled is false", () => {
49
- config.setConfig({ enabled: false });
50
- setOverridesForTesting({ [ACP_FLAG_KEY]: true });
51
-
52
- const result = resolveAcpAgent("claude");
53
-
54
- expect(result.ok).toBe(true);
55
- if (!result.ok) return;
56
- expect(result.agent.command).toBe("claude-agent-acp");
57
- });
58
-
59
27
  test("user config wins over default profile", () => {
60
28
  config.setConfig({
61
29
  agents: {
@@ -359,35 +327,11 @@ describe("resolveAcpAgent - missing binary", () => {
359
327
  // ---------------------------------------------------------------------------
360
328
 
361
329
  describe("listAcpAgents", () => {
362
- test("returns enabled: false with empty agents when both the flag and config are off", () => {
363
- config.setConfig({ enabled: false });
364
-
365
- const result = listAcpAgents();
366
-
367
- expect(result.enabled).toBe(false);
368
- expect(result.agents).toEqual([]);
369
- });
370
-
371
- test("returns the catalog when the acp feature flag is on and config.acp.enabled is false", () => {
372
- config.setConfig({ enabled: false });
373
- setOverridesForTesting({ [ACP_FLAG_KEY]: true });
374
-
375
- const result = listAcpAgents();
376
-
377
- expect(result.enabled).toBe(true);
378
- expect(result.agents.map((a) => a.id)).toEqual([
379
- "claude",
380
- "codex",
381
- "gemini",
382
- ]);
383
- });
384
-
385
330
  test("includes all bundled defaults when user config is empty", () => {
386
331
  config.setConfig({ agents: {} });
387
332
 
388
333
  const result = listAcpAgents();
389
334
 
390
- expect(result.enabled).toBe(true);
391
335
  const ids = result.agents.map((a) => a.id);
392
336
  expect(ids).toEqual(["claude", "codex", "gemini"]);
393
337
  for (const entry of result.agents) {
@@ -3,15 +3,13 @@
3
3
  *
4
4
  * `resolveAcpAgent(id)` merges user-provided `config.acp.agents[id]` (wins on
5
5
  * overlap) with the bundled `DEFAULT_ACP_AGENT_PROFILES` so common agents like
6
- * `claude` and `codex` Just Work whenever ACP is enabled (the `acp` feature
7
- * flag or `acp.enabled: true`; see `feature-gate.ts`), with no per-user
8
- * config required. Natural names ("claude code", "Gemini CLI") resolve via
9
- * `AGENT_ID_ALIASES` when the raw id misses both maps. The result is a
10
- * discriminated union covering every reason
11
- * a spawn might fail before we even start the agent process: ACP disabled,
12
- * unknown agent id, or binary missing from PATH. Callers (acp_spawn,
13
- * acp_list_agents, and the `/v1/acp/spawn` HTTP route) get a single source
14
- * of truth and matching actionable hints.
6
+ * `claude` and `codex` Just Work with no per-user config required. Natural
7
+ * names ("claude code", "Gemini CLI") resolve via `AGENT_ID_ALIASES` when the
8
+ * raw id misses both maps. The result is a discriminated union covering every
9
+ * reason a spawn might fail before we even start the agent process: unknown
10
+ * agent id, or binary missing from PATH. Callers (acp_spawn, acp_list_agents,
11
+ * and the `/v1/acp/spawn` HTTP route) get a single source of truth and
12
+ * matching actionable hints.
15
13
  *
16
14
  * The resolver NEVER fetches or runs packages in the (untrusted) task cwd.
17
15
  * When the adapter binary is missing, resolution simply fails with
@@ -33,7 +31,6 @@ import {
33
31
  } from "../config/acp-defaults.js";
34
32
  import type { AcpAgentConfig } from "../config/acp-schema.js";
35
33
  import { getConfig } from "../config/loader.js";
36
- import { isAcpEnabled } from "./feature-gate.js";
37
34
 
38
35
  /**
39
36
  * Whether this agent's entry came from user config (wins over default) or
@@ -47,7 +44,6 @@ export type ResolveAcpAgentResult =
47
44
  | ResolveAcpAgentFailure;
48
45
 
49
46
  export type ResolveAcpAgentFailure =
50
- | { ok: false; reason: "acp_disabled"; hint: string }
51
47
  | { ok: false; reason: "unknown_agent"; available: string[] }
52
48
  | {
53
49
  ok: false;
@@ -68,8 +64,6 @@ export function formatResolveFailure(
68
64
  failure: ResolveAcpAgentFailure,
69
65
  ): string {
70
66
  switch (failure.reason) {
71
- case "acp_disabled":
72
- return failure.hint;
73
67
  case "unknown_agent":
74
68
  return `Unknown agent "${agentId}". Available: ${failure.available.join(", ")}.`;
75
69
  case "binary_not_found":
@@ -93,14 +87,6 @@ interface AcpAgentEntry {
93
87
  setupHint?: string;
94
88
  }
95
89
 
96
- /**
97
- * Single-source-of-truth hint for "ACP is disabled". Exported so any caller
98
- * that surfaces a disabled-state message (resolver, list-agents tool) reads
99
- * the same string instead of duplicating near-identical copy.
100
- */
101
- export const ACP_DISABLED_HINT =
102
- "Enable the \"ACP Coding Agents\" feature flag in the client's feature flags UI (or set 'acp.enabled': true in ~/.vellum/workspace/config.json).";
103
-
104
90
  function installHintFor(command: string): string {
105
91
  const pkg = DEFAULT_AGENT_NPM_PACKAGES[command];
106
92
  return pkg
@@ -209,9 +195,8 @@ function mergedAgentIds(userAgents: Record<string, AcpAgentConfig>): string[] {
209
195
  * Resolve an ACP agent id to its config + binary preflight result.
210
196
  *
211
197
  * Order of checks:
212
- * 1. ACP must be enabled (feature flag or config; see `isAcpEnabled`).
213
- * 2. The id must resolve to an agent (user config wins; falls back to defaults).
214
- * 3. The agent must be runnable: its `command` on PATH (see
198
+ * 1. The id must resolve to an agent (user config wins; falls back to defaults).
199
+ * 2. The agent must be runnable: its `command` on PATH (see
215
200
  * `resolveRunnableAgent`).
216
201
  *
217
202
  * Each failure mode carries an actionable hint so callers can surface a
@@ -219,10 +204,6 @@ function mergedAgentIds(userAgents: Record<string, AcpAgentConfig>): string[] {
219
204
  */
220
205
  export function resolveAcpAgent(id: string): ResolveAcpAgentResult {
221
206
  const config = getConfig();
222
- if (!isAcpEnabled(config)) {
223
- return { ok: false, reason: "acp_disabled", hint: ACP_DISABLED_HINT };
224
- }
225
-
226
207
  const userAgents = config.acp.agents;
227
208
  const found = lookupAgent(userAgents, id);
228
209
  if (!found) {
@@ -252,20 +233,11 @@ export function resolveAcpAgent(id: string): ResolveAcpAgentResult {
252
233
  * plus any user-only entries — with per-entry availability info. Used by the
253
234
  * `acp_list_agents` tool to render setup steps when an agent's binary isn't
254
235
  * installed yet.
255
- *
256
- * `enabled: false` short-circuits and returns an empty catalog so the tool
257
- * can render a single "ACP is disabled" hint instead of advertising agents
258
- * the user can't actually run.
259
236
  */
260
237
  export function listAcpAgents(): {
261
- enabled: boolean;
262
238
  agents: AcpAgentEntry[];
263
239
  } {
264
240
  const config = getConfig();
265
- if (!isAcpEnabled(config)) {
266
- return { enabled: false, agents: [] };
267
- }
268
-
269
241
  const userAgents = config.acp.agents;
270
242
  const agents: AcpAgentEntry[] = mergedAgentIds(userAgents).map((id) => {
271
243
  // Non-null: ids come from `mergedAgentIds` so the lookup always resolves.
@@ -288,5 +260,5 @@ export function listAcpAgents(): {
288
260
  return entry;
289
261
  });
290
262
 
291
- return { enabled: true, agents };
263
+ return { agents };
292
264
  }
package/src/agent/loop.ts CHANGED
@@ -10,27 +10,22 @@ import {
10
10
  estimateToolsTokens,
11
11
  getCalibrationProviderKey,
12
12
  } from "../context/token-estimator.js";
13
- import type { ContextWindowResult } from "../context/window-manager.js";
14
13
  import type { InboundActorContext } from "../daemon/conversation-runtime-assembly.js";
15
14
  import type { ToolActivityMetadata } from "../daemon/message-types/web-activity.js";
16
15
  import type { TrustContext } from "../daemon/trust-context.js";
17
16
  import { stripHistoricalWebSearchResults } from "../daemon/web-search-history.js";
18
17
  import { HOOKS } from "../plugin-api/constants.js";
19
18
  import type {
20
- AssistantMessageContext,
19
+ PostModelCallContext,
21
20
  PostToolUseContext,
22
21
  PreModelCallContext,
23
22
  StopContext,
24
23
  } from "../plugin-api/types.js";
25
- import {
26
- DEFAULT_COMPACTION_PLUGIN_NAME,
27
- defaultCompact,
28
- } from "../plugins/defaults/compaction/compact.js";
29
- import { getContextWindowManager } from "../plugins/defaults/compaction/manager-store.js";
24
+ import { defaultCompact } from "../plugins/defaults/compaction/compact.js";
25
+ import type { ContextWindowResult } from "../plugins/defaults/compaction/window-manager.js";
30
26
  import postCompact from "../plugins/defaults/memory-retrieval/hooks/post-compact.js";
31
27
  import { runHook } from "../plugins/pipeline.js";
32
28
  import type { CompactionCircuitEvent } from "../plugins/types.js";
33
- import { PluginExecutionError } from "../plugins/types.js";
34
29
  import { normalizeThinkingConfigForWire } from "../providers/thinking-config.js";
35
30
  import type {
36
31
  ContentBlock,
@@ -730,17 +725,8 @@ export class AgentLoop {
730
725
  // Record the history-stripped marker right after stripping, before the
731
726
  // pipeline runs.
732
727
  await onEvent({ type: "history_stripped" });
733
- // The compaction module owns the per-conversation manager; resolve it from
734
- // the store rather than holding a handle on the loop. Absent for callers
735
- // without a compaction path (agent wakes, standalone unit tests), which
736
- // never reach this gate.
737
- const manager = getContextWindowManager(this.conversationId);
738
- if (manager == null) {
739
- throw new PluginExecutionError(
740
- `default-compaction: no ContextWindowManager registered for conversation ${this.conversationId} — the compaction store must construct one before compaction runs`,
741
- DEFAULT_COMPACTION_PLUGIN_NAME,
742
- );
743
- }
728
+ // The compaction module owns the per-conversation manager; pass the
729
+ // conversation id and let `defaultCompact` resolve it from the store.
744
730
  // The mid-loop budget gate is reached only when this turn decides to
745
731
  // compact in place, so `force` past the auto-threshold check.
746
732
  // `actorTrustClass` comes from the turn's trust snapshot (the actor whose
@@ -748,7 +734,7 @@ export class AgentLoop {
748
734
  // guardian-only attachments for untrusted actors. `overrideProfile` is the
749
735
  // turn's resolved inference-profile override for the summary call.
750
736
  const compactResult = await defaultCompact({
751
- manager,
737
+ conversationId: this.conversationId,
752
738
  messages: rawHistory,
753
739
  signal,
754
740
  force: true,
@@ -1114,7 +1100,7 @@ export class AgentLoop {
1114
1100
 
1115
1101
  // A `pre-model-call` hook (below) can defer this turn's assistant
1116
1102
  // output; when set, the live text stream is held so an
1117
- // `assistant-message` hook can emit the finalized (transformed) text
1103
+ // `post-model-call` hook can emit the finalized (transformed) text
1118
1104
  // instead. Reset per model call.
1119
1105
  let deferAssistantOutput = false;
1120
1106
 
@@ -1128,7 +1114,7 @@ export class AgentLoop {
1128
1114
  onEvent: (event) => {
1129
1115
  if (event.type === "text_delta") {
1130
1116
  // Held when the turn's output is deferred — the final text is
1131
- // emitted once, after the `assistant-message` hook runs.
1117
+ // emitted once, after the `post-model-call` hook runs.
1132
1118
  if (deferAssistantOutput) return;
1133
1119
  // Apply sensitive-output placeholder substitution (chunk-safe)
1134
1120
  if (substitutionMap.size > 0) {
@@ -1298,7 +1284,7 @@ export class AgentLoop {
1298
1284
  streamingPending = "";
1299
1285
  }
1300
1286
 
1301
- // Run the `assistant-message` hook on a finalized message and, when
1287
+ // Run the `post-model-call` hook on a finalized message and, when
1302
1288
  // output was deferred, emit the finalized text once (with sensitive-output
1303
1289
  // substitution applied, matching the live stream). Fail-open: the hook
1304
1290
  // receives a clone, so a throw — even mid in-place mutation — leaves the
@@ -1308,19 +1294,19 @@ export class AgentLoop {
1308
1294
  ): Promise<Message> => {
1309
1295
  let finalized = message;
1310
1296
  try {
1311
- const ctx: AssistantMessageContext = {
1297
+ const ctx: PostModelCallContext = {
1312
1298
  conversationId: this.conversationId,
1313
1299
  callSite,
1314
1300
  content: structuredClone(message.content),
1315
1301
  stopReason: response.stopReason,
1316
1302
  logger: rlog,
1317
1303
  };
1318
- const result = await runHook(HOOKS.ASSISTANT_MESSAGE, ctx);
1304
+ const result = await runHook(HOOKS.POST_MODEL_CALL, ctx);
1319
1305
  finalized = { role: "assistant", content: result.content };
1320
1306
  } catch (assistantMessageError) {
1321
1307
  rlog.error(
1322
1308
  { err: assistantMessageError },
1323
- "assistant-message hook failed — keeping the original content",
1309
+ "post-model-call hook failed — keeping the original content",
1324
1310
  );
1325
1311
  finalized = message;
1326
1312
  }
@@ -1460,7 +1446,7 @@ export class AgentLoop {
1460
1446
  }
1461
1447
  }
1462
1448
 
1463
- // Run the `assistant-message` hook + emit any deferred final text.
1449
+ // Run the `post-model-call` hook + emit any deferred final text.
1464
1450
  // On a no-tool turn this point is reached only after the `stop` hook
1465
1451
  // resolves to "stop" (a `continue` already re-queried above), so a
1466
1452
  // re-queried reply is never transformed-then-discarded.