@vellumai/assistant 0.8.7-dev.202606052232.2ddc989 → 0.8.8

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 (262) hide show
  1. package/bun.lock +2 -2
  2. package/docs/plugins.md +832 -0
  3. package/examples/plugins/echo/README.md +60 -61
  4. package/examples/plugins/echo/package.json +2 -1
  5. package/examples/plugins/echo/register.ts +143 -0
  6. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +6 -7
  7. package/openapi.yaml +5 -15
  8. package/package.json +2 -2
  9. package/src/__tests__/agent-loop-exit-reason.test.ts +56 -3
  10. package/src/__tests__/anthropic-provider.test.ts +1 -1
  11. package/src/__tests__/app-control-flow.test.ts +1 -1
  12. package/src/__tests__/app-dir-path-guard.test.ts +0 -1
  13. package/src/__tests__/approval-routes-http.test.ts +1 -4
  14. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  15. package/src/__tests__/channel-approvals.test.ts +1 -1
  16. package/src/__tests__/circuit-breaker-pipeline.test.ts +405 -0
  17. package/src/__tests__/compaction-pipeline.test.ts +210 -0
  18. package/src/__tests__/compaction-timeout-recovery.test.ts +251 -0
  19. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +3 -0
  20. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +3 -0
  21. package/src/__tests__/conversation-agent-loop-overflow.test.ts +7 -3
  22. package/src/__tests__/conversation-agent-loop.test.ts +39 -42
  23. package/src/__tests__/conversation-clean-command.test.ts +2 -5
  24. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -4
  25. package/src/__tests__/conversation-runtime-assembly.test.ts +71 -140
  26. package/src/__tests__/conversation-runtime-workspace.test.ts +27 -108
  27. package/src/__tests__/conversation-starter-routes.test.ts +6 -14
  28. package/src/__tests__/conversation-workspace-cache-state.test.ts +16 -17
  29. package/src/__tests__/conversation-workspace-injection.test.ts +1 -61
  30. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -7
  31. package/src/__tests__/db-acp-history.test.ts +0 -101
  32. package/src/__tests__/dynamic-page-surface.test.ts +0 -31
  33. package/src/__tests__/file-write-tool.test.ts +0 -63
  34. package/src/__tests__/gateway-only-guard.test.ts +2 -12
  35. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  36. package/src/__tests__/guardian-routing-invariants.test.ts +4 -2
  37. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  38. package/src/__tests__/heartbeat-disk-pressure.test.ts +0 -1
  39. package/src/__tests__/heartbeat-service.test.ts +0 -1
  40. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  41. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  42. package/src/__tests__/injector-background-turn.test.ts +1 -1
  43. package/src/__tests__/injector-chain.test.ts +6 -34
  44. package/src/__tests__/injector-disk-pressure.test.ts +34 -77
  45. package/src/__tests__/injector-document-comments.test.ts +1 -1
  46. package/src/__tests__/list-messages-hidden-metadata.test.ts +0 -38
  47. package/src/__tests__/memory-v2-static-injector.test.ts +1 -1
  48. package/src/__tests__/{overflow-reduction-loop.test.ts → overflow-reduce-pipeline.test.ts} +284 -64
  49. package/src/__tests__/pipeline-runner.test.ts +554 -0
  50. package/src/__tests__/plugin-api-shim.test.ts +6 -3
  51. package/src/__tests__/plugin-bootstrap.test.ts +23 -12
  52. package/src/__tests__/plugin-registry.test.ts +49 -3
  53. package/src/__tests__/plugin-types.test.ts +70 -0
  54. package/src/__tests__/reaction-persistence.test.ts +1 -1
  55. package/src/__tests__/send-endpoint-busy.test.ts +1 -4
  56. package/src/__tests__/skill-feature-flags-integration.test.ts +0 -33
  57. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  58. package/src/__tests__/subagent-fork-notifications.test.ts +3 -1
  59. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  60. package/src/__tests__/subagent-manager-notify.test.ts +3 -1
  61. package/src/__tests__/subagent-notify-parent.test.ts +3 -1
  62. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  63. package/src/__tests__/user-plugin-loader.test.ts +286 -54
  64. package/src/acp/__tests__/client-handler.test.ts +0 -40
  65. package/src/acp/__tests__/prepare-agent-env.test.ts +0 -137
  66. package/src/acp/__tests__/session-manager-persistence.test.ts +28 -95
  67. package/src/acp/agent-process.ts +1 -61
  68. package/src/acp/client-handler.ts +0 -31
  69. package/src/acp/prepare-agent-env.ts +29 -83
  70. package/src/acp/resolve-agent.test.ts +7 -320
  71. package/src/acp/resolve-agent.ts +18 -182
  72. package/src/acp/session-manager.ts +73 -495
  73. package/src/acp/types.ts +0 -8
  74. package/src/agent/compaction-circuit.ts +102 -60
  75. package/src/agent/loop.ts +59 -32
  76. package/src/api/responses/conversation-message.ts +1 -7
  77. package/src/approvals/guardian-request-resolvers.ts +1 -1
  78. package/src/background-wake/next-wake.ts +0 -1
  79. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  80. package/src/config/acp-defaults.test.ts +0 -10
  81. package/src/config/acp-defaults.ts +0 -6
  82. package/src/config/bundled-skills/acp/SKILL.md +31 -83
  83. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  84. package/src/config/bundled-skills/app-builder/SKILL.md +381 -224
  85. package/src/config/bundled-skills/app-builder/TOOLS.json +0 -29
  86. package/src/config/bundled-skills/document-editor/SKILL.md +23 -28
  87. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  88. package/src/config/bundled-tool-registry.ts +0 -2
  89. package/src/config/feature-flag-registry.json +5 -14
  90. package/src/config/schemas/heartbeat.ts +0 -9
  91. package/src/context/strip-injections.ts +2 -8
  92. package/src/context/window-manager.ts +1 -2
  93. package/src/daemon/conversation-agent-loop-handlers.ts +11 -0
  94. package/src/daemon/conversation-agent-loop.ts +279 -62
  95. package/src/daemon/conversation-runtime-assembly.ts +69 -106
  96. package/src/daemon/conversation-store.ts +90 -9
  97. package/src/daemon/conversation-workspace.ts +0 -17
  98. package/src/daemon/conversation.ts +6 -0
  99. package/src/daemon/external-plugins-bootstrap.ts +11 -11
  100. package/src/daemon/handlers/conversations.ts +1 -3
  101. package/src/daemon/handlers/skills.ts +1 -4
  102. package/src/daemon/lifecycle.ts +0 -21
  103. package/src/daemon/server.ts +0 -2
  104. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -3
  105. package/src/heartbeat/heartbeat-run-store.ts +1 -23
  106. package/src/heartbeat/heartbeat-service.ts +0 -26
  107. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  108. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  109. package/src/ipc/skill-routes/__tests__/memory.test.ts +0 -15
  110. package/src/ipc/skill-routes/memory.ts +2 -4
  111. package/src/memory/conversation-starter-checkpoints.ts +0 -1
  112. package/src/memory/db-init.ts +0 -2
  113. package/src/memory/job-handlers/conversation-starters.ts +2 -13
  114. package/src/memory/jobs-worker.ts +1 -1
  115. package/src/memory/migrations/index.ts +0 -1
  116. package/src/memory/schema/acp.ts +0 -4
  117. package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
  118. package/src/memory/v2/consolidation-job.ts +4 -13
  119. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/assign.test.ts +4 -4
  120. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/live-integration.test.ts +4 -4
  121. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/maintain-job.test.ts +5 -5
  122. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/orchestrate.test.ts +3 -3
  123. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/reconcile.test.ts +2 -2
  124. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/render-injection.test.ts +1 -1
  125. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/router.test.ts +3 -3
  126. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/selection-log-store.test.ts +8 -8
  127. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/selector.test.ts +3 -3
  128. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/shadow-plugin.test.ts +12 -12
  129. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/assign.ts +5 -5
  130. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/capabilities.ts +2 -2
  131. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/maintain-job.ts +8 -8
  132. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/page-content.ts +2 -2
  133. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/provider-blocks.ts +1 -1
  134. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/reconcile.ts +3 -3
  135. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/render-injection.ts +1 -1
  136. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/router.ts +3 -3
  137. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/selection-log-store.ts +4 -4
  138. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/selector.ts +4 -4
  139. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/shadow-plugin.ts +90 -28
  140. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/tree.ts +1 -1
  141. package/src/plugin-api/index.ts +5 -0
  142. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +93 -0
  143. package/src/plugins/defaults/{memory-v3-shadow → circuit-breaker}/package.json +2 -2
  144. package/src/plugins/defaults/circuit-breaker/register.ts +39 -0
  145. package/src/plugins/defaults/compaction/middlewares/compaction.ts +25 -0
  146. package/src/plugins/defaults/compaction/package.json +1 -1
  147. package/src/plugins/defaults/compaction/register.ts +19 -8
  148. package/src/plugins/defaults/compaction/terminal.ts +73 -0
  149. package/src/plugins/defaults/index.ts +5 -3
  150. package/src/plugins/defaults/{memory-retrieval/injectors.ts → injectors/register.ts} +7 -45
  151. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +7 -11
  152. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
  153. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +126 -0
  154. package/src/plugins/defaults/overflow-reduce/package.json +15 -0
  155. package/src/plugins/defaults/overflow-reduce/register.ts +42 -0
  156. package/src/plugins/external-api.ts +2 -2
  157. package/src/plugins/pipeline.ts +293 -6
  158. package/src/plugins/registry.ts +37 -9
  159. package/src/plugins/types.ts +336 -32
  160. package/src/plugins/user-loader.ts +127 -30
  161. package/src/proactive-artifact/aux-message-injector.ts +1 -1
  162. package/src/proactive-artifact/job.test.ts +1 -1
  163. package/src/prompts/__tests__/system-prompt.test.ts +0 -6
  164. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +2 -4
  165. package/src/runtime/__tests__/agent-wake.test.ts +5 -5
  166. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  167. package/src/runtime/agent-wake.ts +3 -0
  168. package/src/runtime/assistant-event-hub.ts +1 -1
  169. package/src/runtime/channel-approvals.ts +1 -1
  170. package/src/runtime/interactive-ui.ts +1 -1
  171. package/src/runtime/routes/__tests__/acp-routes.test.ts +55 -283
  172. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  173. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +4 -5
  174. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +1 -4
  175. package/src/runtime/routes/acp-routes.test.ts +25 -89
  176. package/src/runtime/routes/acp-routes.ts +29 -81
  177. package/src/runtime/routes/approval-routes.ts +1 -1
  178. package/src/runtime/routes/browser-routes.ts +1 -1
  179. package/src/runtime/routes/browser-tabs-routes.ts +10 -6
  180. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  181. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  182. package/src/runtime/routes/conversation-query-routes.ts +1 -1
  183. package/src/runtime/routes/conversation-routes.ts +2 -15
  184. package/src/runtime/routes/conversation-starter-routes.ts +7 -13
  185. package/src/runtime/routes/conversations-import-routes.ts +7 -24
  186. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  187. package/src/runtime/routes/host-cu-routes.ts +1 -1
  188. package/src/runtime/routes/identity-routes.ts +3 -18
  189. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  190. package/src/runtime/routes/memory-v3-routes.ts +6 -16
  191. package/src/runtime/routes/playground/helpers.ts +1 -1
  192. package/src/runtime/routes/surface-conversation-resolver.ts +3 -4
  193. package/src/runtime/routes/work-items-routes.ts +4 -2
  194. package/src/runtime/services/conversation-serializer.ts +1 -1
  195. package/src/signals/cancel.ts +4 -2
  196. package/src/subagent/manager.ts +5 -17
  197. package/src/tools/acp/list-agents.test.ts +1 -7
  198. package/src/tools/acp/spawn.test.ts +55 -158
  199. package/src/tools/acp/spawn.ts +72 -47
  200. package/src/tools/acp/steer.test.ts +8 -105
  201. package/src/tools/acp/steer.ts +17 -48
  202. package/src/tools/apps/executors.ts +8 -13
  203. package/src/tools/filesystem/write.ts +0 -34
  204. package/src/tools/subagent/spawn.ts +4 -2
  205. package/src/tools/ui-surface/definitions.ts +4 -25
  206. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +5 -4
  207. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +45 -69
  208. package/examples/plugins/echo/hooks/post-tool-use.ts +0 -18
  209. package/examples/plugins/echo/hooks/stop.ts +0 -16
  210. package/examples/plugins/echo/hooks/user-prompt-submit.ts +0 -18
  211. package/examples/plugins/echo/src/emit.ts +0 -19
  212. package/src/__tests__/compaction-circuit.test.ts +0 -258
  213. package/src/__tests__/compaction-direct.test.ts +0 -132
  214. package/src/__tests__/conversations-import-system-filter.test.ts +0 -101
  215. package/src/acp/__tests__/agent-process.test.ts +0 -161
  216. package/src/acp/__tests__/helpers/acp-history-db.ts +0 -82
  217. package/src/acp/__tests__/helpers/exec-file-stub.ts +0 -101
  218. package/src/acp/__tests__/session-manager-resume.test.ts +0 -736
  219. package/src/acp/auto-install.test.ts +0 -196
  220. package/src/acp/auto-install.ts +0 -177
  221. package/src/acp/feature-gate.test.ts +0 -48
  222. package/src/acp/feature-gate.ts +0 -34
  223. package/src/acp/resume-hint.ts +0 -25
  224. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +0 -48
  225. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +0 -57
  226. package/src/config/bundled-skills/app-builder/references/SLIDES.md +0 -38
  227. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -62
  228. package/src/daemon/conversation-registry.ts +0 -159
  229. package/src/daemon/overflow-reduction-loop.ts +0 -230
  230. package/src/memory/migrations/272-acp-session-history-cwd.ts +0 -36
  231. package/src/plugins/defaults/compaction/compact.ts +0 -59
  232. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +0 -14
  233. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +0 -19
  234. package/src/plugins/defaults/memory-v3-shadow/injector.ts +0 -75
  235. package/src/plugins/defaults/memory-v3-shadow/register.ts +0 -26
  236. package/src/tools/acp/context.ts +0 -20
  237. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/capabilities.test.ts +0 -0
  238. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/core.test.ts +0 -0
  239. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/fixtures/eval-turns.json +0 -0
  240. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/fixtures/live-turns.json +0 -0
  241. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/health.test.ts +0 -0
  242. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/needle.test.ts +0 -0
  243. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/provider-blocks.test.ts +0 -0
  244. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/snapshot.test.ts +0 -0
  245. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/tree.test.ts +0 -0
  246. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/types.test.ts +0 -0
  247. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/working-set-eviction.test.ts +0 -0
  248. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/working-set-skeleton.test.ts +0 -0
  249. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/core.ts +0 -0
  250. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/README.md +0 -0
  251. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/assignments.json +0 -0
  252. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/core.json +0 -0
  253. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-a/topic-x.md +0 -0
  254. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-a/topic-y.md +0 -0
  255. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-b/topic-z.md +0 -0
  256. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/health.ts +0 -0
  257. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/llm-retry.ts +0 -0
  258. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/needle.ts +0 -0
  259. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/orchestrate.ts +0 -0
  260. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/snapshot.ts +0 -0
  261. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/types.ts +0 -0
  262. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/working-set.ts +0 -0
@@ -13,11 +13,7 @@ import { v4 as uuid } from "uuid";
13
13
  import { resolveCallSiteConfig } from "../config/llm-resolver.js";
14
14
  import { getConfig } from "../config/loader.js";
15
15
  import { Conversation } from "../daemon/conversation.js";
16
- import {
17
- findConversation,
18
- removeSubagentConversation,
19
- setSubagentConversation,
20
- } from "../daemon/conversation-registry.js";
16
+ import { findConversation } from "../daemon/conversation-store.js";
21
17
  import type { ServerMessage } from "../daemon/message-protocol.js";
22
18
  import { bootstrapConversation } from "../memory/conversation-bootstrap.js";
23
19
  import { wrapWithCallSiteRouting } from "../providers/call-site-routing.js";
@@ -338,10 +334,6 @@ export class SubagentManager {
338
334
 
339
335
  managed.conversation = conversation;
340
336
  this.subagents.set(subagentId, managed);
341
- // Index the live conversation so the per-conversation injectors (workspace
342
- // context, disk-pressure warning) can resolve it by id; subagents are not
343
- // in the eviction-managed conversation store.
344
- setSubagentConversation(conversationRecord.id, conversation);
345
337
  const labelKey = `${config.parentConversationId}:${config.label.toLowerCase().trim()}`;
346
338
  if (this.labelIndex.has(labelKey)) {
347
339
  log.warn(
@@ -703,9 +695,7 @@ export class SubagentManager {
703
695
  */
704
696
  private releaseConversation(managed: ManagedSubagent): void {
705
697
  if (!managed.conversation) return;
706
- const conversation = managed.conversation;
707
- removeSubagentConversation(conversation.conversationId, conversation);
708
- conversation.dispose();
698
+ managed.conversation.dispose();
709
699
  managed.conversation = null;
710
700
  managed.retainedUntil = Date.now() + TERMINAL_RETENTION_MS;
711
701
  this.ensureSweepRunning();
@@ -726,18 +716,16 @@ export class SubagentManager {
726
716
  if (!managed) return;
727
717
 
728
718
  if (managed.conversation) {
729
- const conversation = managed.conversation;
730
719
  if (!TERMINAL_STATUSES.has(managed.state.status)) {
731
- conversation.abort(
720
+ managed.conversation.abort(
732
721
  createAbortReason(
733
722
  "subagent_aborted",
734
723
  "SubagentManager.dispose",
735
- conversation.conversationId,
724
+ managed.conversation.conversationId,
736
725
  ),
737
726
  );
738
727
  }
739
- removeSubagentConversation(conversation.conversationId, conversation);
740
- conversation.dispose();
728
+ managed.conversation.dispose();
741
729
  managed.conversation = null;
742
730
  }
743
731
  this.subagents.delete(subagentId);
@@ -47,7 +47,7 @@ describe("executeAcpListAgents", () => {
47
47
  expect(parsed.hint).toContain("config.json");
48
48
  });
49
49
 
50
- test("enabled, no user config: all defaults present with source 'default' and available based on Bun.which", async () => {
50
+ test("enabled, no user config: both defaults present with source 'default' and available based on Bun.which", async () => {
51
51
  config.setConfig({ agents: {} });
52
52
 
53
53
  const result = await executeAcpListAgents({}, makeContext());
@@ -58,7 +58,6 @@ describe("executeAcpListAgents", () => {
58
58
  expect(parsed.agents.map((a: { id: string }) => a.id)).toEqual([
59
59
  "claude",
60
60
  "codex",
61
- "gemini",
62
61
  ]);
63
62
  for (const entry of parsed.agents) {
64
63
  expect(entry.source).toBe("default");
@@ -109,11 +108,6 @@ describe("executeAcpListAgents", () => {
109
108
  expect(codex.unavailableReason).toBe("'codex-acp' is not on PATH");
110
109
  expect(codex.setupHint).toBe("npm i -g @zed-industries/codex-acp");
111
110
 
112
- const gemini = parsed.agents.find((a: { id: string }) => a.id === "gemini");
113
- expect(gemini.available).toBe(false);
114
- expect(gemini.unavailableReason).toBe("'gemini' is not on PATH");
115
- expect(gemini.setupHint).toBe("npm i -g @google/gemini-cli");
116
-
117
111
  const claude = parsed.agents.find((a: { id: string }) => a.id === "claude");
118
112
  expect(claude.available).toBe(true);
119
113
  expect(claude.setupHint).toBeUndefined();
@@ -1,7 +1,7 @@
1
+ import * as realChildProcess from "node:child_process";
1
2
  import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
2
3
 
3
4
  import { installAcpConfigStub } from "../../acp/__tests__/helpers/acp-config-stub.js";
4
- import { installExecFileStub } from "../../acp/__tests__/helpers/exec-file-stub.js";
5
5
  import { installWhichStub } from "../../acp/__tests__/helpers/which-stub.js";
6
6
  import type { ToolContext } from "../types.js";
7
7
 
@@ -9,11 +9,55 @@ import type { ToolContext } from "../types.js";
9
9
  // Mock infrastructure
10
10
  // ---------------------------------------------------------------------------
11
11
 
12
- const {
13
- execScripts,
14
- execFileMock,
15
- reset: resetExecFileStub,
16
- } = installExecFileStub();
12
+ type ExecCallback = (
13
+ err: Error | null,
14
+ stdout: string | Buffer,
15
+ stderr: string | Buffer,
16
+ ) => void;
17
+
18
+ interface ExecScript {
19
+ /** When set, the call rejects with this error. */
20
+ error?: Error;
21
+ /** When set, the call resolves with this stdout. */
22
+ stdout?: string;
23
+ }
24
+
25
+ /**
26
+ * Per-call scripted responses for `execFile`. Keyed by `${command} ${args[0]}`
27
+ * so tests can target `npm ls` and `npm view` independently.
28
+ */
29
+ const execScripts: Map<string, ExecScript> = new Map();
30
+
31
+ const execFileMock = mock(
32
+ (
33
+ command: string,
34
+ args: string[],
35
+ _options: unknown,
36
+ callback?: ExecCallback,
37
+ ) => {
38
+ const key = `${command} ${args[0]}`;
39
+ const script = execScripts.get(key);
40
+ queueMicrotask(() => {
41
+ if (!callback) return;
42
+ if (!script) {
43
+ callback(new Error(`No script for ${key}`), "", "");
44
+ return;
45
+ }
46
+ if (script.error) {
47
+ callback(script.error, "", "");
48
+ return;
49
+ }
50
+ callback(null, script.stdout ?? "", "");
51
+ });
52
+ // Return value is not used by execFileWithTimeout.
53
+ return {} as ReturnType<typeof realChildProcess.execFile>;
54
+ },
55
+ );
56
+
57
+ mock.module("node:child_process", () => ({
58
+ ...realChildProcess,
59
+ execFile: execFileMock,
60
+ }));
17
61
 
18
62
  // Default ACP config used by these tests: the `unknown-agent` entry is here
19
63
  // to give the "no version check" test a configured agent whose binary isn't
@@ -145,9 +189,6 @@ mock.module("../../acp/index.js", () => ({
145
189
 
146
190
  const { executeAcpSpawn, _resetAdapterVersionCacheForTests } =
147
191
  await import("./spawn.js");
148
- const { _resetAdapterInstallCacheForTests } = await import(
149
- "../../acp/auto-install.js"
150
- );
151
192
 
152
193
  // ---------------------------------------------------------------------------
153
194
  // Helpers
@@ -163,10 +204,10 @@ function makeContext(): ToolContext {
163
204
  }
164
205
 
165
206
  beforeEach(() => {
166
- resetExecFileStub();
207
+ execScripts.clear();
208
+ execFileMock.mockClear();
167
209
  spawnMock.mockClear();
168
210
  _resetAdapterVersionCacheForTests();
169
- _resetAdapterInstallCacheForTests();
170
211
  config.setConfig({ agents: DEFAULT_TEST_AGENTS });
171
212
  which.setWhich((cmd) => `/usr/local/bin/${cmd}`);
172
213
  // Default: vault has a claude token so the preflight in `prepareAgentEnv`
@@ -350,9 +391,8 @@ describe("executeAcpSpawn — input validation", () => {
350
391
  expect(result.content).toContain("acp.enabled");
351
392
  });
352
393
 
353
- test("missing binary returns install hint when auto-install fails", async () => {
394
+ test("missing binary returns install hint", async () => {
354
395
  which.setWhich({});
355
- execScripts.set("npm i", { error: new Error("npm not installed") });
356
396
  const result = await executeAcpSpawn(
357
397
  { agent: "claude", task: "do something" },
358
398
  makeContext(),
@@ -386,149 +426,6 @@ describe("executeAcpSpawn — input validation", () => {
386
426
  });
387
427
  });
388
428
 
389
- describe("executeAcpSpawn: auto-install on missing binary", () => {
390
- test("known command: installs the mapped package and spawn proceeds with a note", async () => {
391
- // Binary appears on PATH only after `npm i -g` runs, simulating a
392
- // successful global install.
393
- let binaryOnPath = false;
394
- which.setWhich((cmd) => (binaryOnPath ? `/usr/local/bin/${cmd}` : null));
395
- execScripts.set("npm i", {
396
- stdout: "",
397
- onCall: () => {
398
- binaryOnPath = true;
399
- },
400
- });
401
- // Version probes after the install: best-effort, scripted to skip.
402
- execScripts.set("npm ls", { error: new Error("skip") });
403
- execScripts.set("npm view", { error: new Error("skip") });
404
-
405
- const result = await executeAcpSpawn(
406
- { agent: "claude", task: "do something" },
407
- makeContext(),
408
- );
409
-
410
- expect(result.isError).toBe(false);
411
- expect(spawnMock).toHaveBeenCalledTimes(1);
412
- const [payloadJson] = result.content.split("\n\n");
413
- const payload = JSON.parse(payloadJson);
414
- expect(payload.message).toContain(
415
- "Installed @agentclientprotocol/claude-agent-acp automatically.",
416
- );
417
- const installCalls = execFileMock.mock.calls.filter(
418
- (call) => (call[1] as string[])[0] === "i",
419
- );
420
- expect(installCalls).toHaveLength(1);
421
- expect(installCalls[0][1]).toEqual([
422
- "i",
423
- "-g",
424
- "@agentclientprotocol/claude-agent-acp",
425
- ]);
426
- });
427
-
428
- test("unknown command: no install attempted, plain hint returned", async () => {
429
- which.setWhich({});
430
-
431
- const result = await executeAcpSpawn(
432
- { agent: "unknown-agent", task: "do something" },
433
- makeContext(),
434
- );
435
-
436
- expect(result.isError).toBe(true);
437
- expect(result.content).toContain("some-other-binary is not on PATH");
438
- expect(result.content).toContain("Install 'some-other-binary'");
439
- expect(result.content).not.toContain("auto-install failed");
440
- expect(execFileMock).not.toHaveBeenCalled();
441
- expect(spawnMock).not.toHaveBeenCalled();
442
- });
443
-
444
- test("no client connected: no install attempted even when the binary is missing", async () => {
445
- // The no-client guard is a pure precondition and must run BEFORE the
446
- // auto-install side effect: without a client the spawn fails anyway, so
447
- // the host must not be mutated by `npm i -g` (which can also block for
448
- // up to the install timeout).
449
- which.setWhich({});
450
-
451
- const result = await executeAcpSpawn(
452
- { agent: "claude", task: "do something" },
453
- { ...makeContext(), sendToClient: undefined },
454
- );
455
-
456
- expect(result.isError).toBe(true);
457
- expect(result.content).toContain("No client connected");
458
- expect(execFileMock).not.toHaveBeenCalled();
459
- expect(spawnMock).not.toHaveBeenCalled();
460
- });
461
-
462
- test("npm failure: hint and install failure both surface", async () => {
463
- which.setWhich({});
464
- execScripts.set("npm i", {
465
- error: new Error("EACCES: permission denied"),
466
- });
467
-
468
- const result = await executeAcpSpawn(
469
- { agent: "claude", task: "do something" },
470
- makeContext(),
471
- );
472
-
473
- expect(result.isError).toBe(true);
474
- expect(result.content).toContain("claude-agent-acp is not on PATH");
475
- expect(result.content).toContain(
476
- "npm i -g @agentclientprotocol/claude-agent-acp",
477
- );
478
- expect(result.content).toContain("auto-install failed");
479
- expect(result.content).toContain("EACCES");
480
- expect(spawnMock).not.toHaveBeenCalled();
481
- });
482
- });
483
-
484
- describe("executeAcpSpawn - bunx fallback when binary missing", () => {
485
- test("binary missing + bun present: spawns via `bun x` with env injection and resume hint, no npm calls", async () => {
486
- // Only bun is on PATH - the platform-hosted image (bun, no node/npm).
487
- which.setWhich({ bun: "/usr/local/bin/bun" });
488
-
489
- const result = await executeAcpSpawn(
490
- { agent: "claude", task: "do something" },
491
- makeContext(),
492
- );
493
-
494
- expect(result.isError).toBe(false);
495
- // No npm install AND no npm version probe: bunx fetches the package on
496
- // first use and there is no global install to compare.
497
- expect(execFileMock).not.toHaveBeenCalled();
498
- expect(result.content).not.toContain("outdated");
499
-
500
- expect(spawnMock).toHaveBeenCalledTimes(1);
501
- const agentConfigArg = spawnMock.mock.calls[0][1] as {
502
- command: string;
503
- args: string[];
504
- adapterCommand?: string;
505
- env?: Record<string, string>;
506
- };
507
- expect(agentConfigArg.command).toBe("bun");
508
- expect(agentConfigArg.args).toEqual([
509
- "x",
510
- "--bun",
511
- "@agentclientprotocol/claude-agent-acp",
512
- ]);
513
- expect(agentConfigArg.adapterCommand).toBe("claude-agent-acp");
514
- // CLAUDE_CODE_OAUTH_TOKEN injection still applies to the bunx-resolved
515
- // claude adapter (gated on adapterCommand, not the spawn command).
516
- expect(agentConfigArg.env?.CLAUDE_CODE_OAUTH_TOKEN).toBe(
517
- "default-test-token",
518
- );
519
-
520
- const [payloadJson] = result.content.split("\n\n");
521
- const payload = JSON.parse(payloadJson);
522
- // No auto-install happened; the claude resume hint still fires.
523
- expect(payload.message).not.toContain("Installed");
524
- expect(payload.message).toContain("claude --resume");
525
- });
526
-
527
- // The bun-absent npm fallback is covered by the existing auto-install
528
- // suite below ("known command: installs the mapped package..."), whose
529
- // which-stub leaves bun off PATH until the install completes.
530
- });
531
-
532
429
  describe("executeAcpSpawn — per-agent resume hint", () => {
533
430
  test("claude payload includes the `claude --resume` hint", async () => {
534
431
  execScripts.set("npm ls", { error: new Error("npm not installed") });
@@ -543,7 +440,7 @@ describe("executeAcpSpawn — per-agent resume hint", () => {
543
440
  const [payloadJson] = result.content.split("\n\n");
544
441
  const payload = JSON.parse(payloadJson);
545
442
  expect(payload.message).toContain("claude --resume");
546
- expect(payload.message).toContain("To resume:");
443
+ expect(payload.message).toContain("To resume this session later");
547
444
  });
548
445
 
549
446
  test("non-claude payload omits the `claude --resume` hint", async () => {
@@ -561,7 +458,7 @@ describe("executeAcpSpawn — per-agent resume hint", () => {
561
458
  const [payloadJson] = result.content.split("\n\n");
562
459
  const payload = JSON.parse(payloadJson);
563
460
  expect(payload.message).not.toContain("claude --resume");
564
- expect(payload.message).not.toContain("To resume:");
461
+ expect(payload.message).not.toContain("To resume this session later");
565
462
  });
566
463
  });
567
464
 
@@ -1,19 +1,11 @@
1
- import {
2
- execFileWithTimeout,
3
- resolveAgentWithAutoInstall,
4
- } from "../../acp/auto-install.js";
1
+ import { execFile } from "node:child_process";
2
+
5
3
  import { getAcpSessionManager } from "../../acp/index.js";
6
4
  import { prepareAgentEnv } from "../../acp/prepare-agent-env.js";
7
- import {
8
- adapterCommandOf,
9
- formatResolveFailure,
10
- runsViaBunx,
11
- } from "../../acp/resolve-agent.js";
12
- import { claudeResumeHint } from "../../acp/resume-hint.js";
5
+ import { resolveAcpAgent } from "../../acp/resolve-agent.js";
13
6
  import { DEFAULT_AGENT_NPM_PACKAGES } from "../../config/acp-defaults.js";
14
7
  import { getLogger } from "../../util/logger.js";
15
8
  import type { ToolContext, ToolExecutionResult } from "../types.js";
16
- import { getSendToClient } from "./context.js";
17
9
 
18
10
  const log = getLogger("acp:spawn");
19
11
 
@@ -33,6 +25,35 @@ interface AdapterVersionInfo {
33
25
  packageName: string;
34
26
  }
35
27
 
28
+ /**
29
+ * Run `execFile` with an AbortController-driven timeout. Returns the stdout
30
+ * on success; throws on error or timeout. Caller treats any throw as a
31
+ * best-effort skip.
32
+ */
33
+ function execFileWithTimeout(
34
+ command: string,
35
+ args: string[],
36
+ timeoutMs: number,
37
+ ): Promise<string> {
38
+ return new Promise((resolve, reject) => {
39
+ const controller = new AbortController();
40
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
41
+ execFile(
42
+ command,
43
+ args,
44
+ { signal: controller.signal, encoding: "utf8" },
45
+ (err, stdout) => {
46
+ clearTimeout(timer);
47
+ if (err) {
48
+ reject(err);
49
+ return;
50
+ }
51
+ resolve(stdout);
52
+ },
53
+ );
54
+ });
55
+ }
56
+
36
57
  /**
37
58
  * Checks if the globally-installed ACP adapter for `command` is outdated.
38
59
  * Best-effort: any error or timeout returns `null` (skipped). Unknown
@@ -114,10 +135,34 @@ export async function executeAcpSpawn(
114
135
  return { content: '"task" is required.', isError: true };
115
136
  }
116
137
 
117
- // Pure precondition: check for a connected client BEFORE any side effects
118
- // (auto-install mutates the host via `npm i -g` and can block for up to
119
- // the install timeout). Without a client the spawn cannot succeed anyway.
120
- const sendToClient = getSendToClient(context);
138
+ const resolved = resolveAcpAgent(agent);
139
+ if (!resolved.ok) {
140
+ switch (resolved.reason) {
141
+ case "acp_disabled":
142
+ return { content: resolved.hint, isError: true };
143
+ case "unknown_agent":
144
+ return {
145
+ content: `Unknown agent "${agent}". Available: ${resolved.available.join(
146
+ ", ",
147
+ )}.`,
148
+ isError: true,
149
+ };
150
+ case "binary_not_found":
151
+ return {
152
+ content: `${resolved.command} is not on PATH. ${resolved.hint}`,
153
+ isError: true,
154
+ };
155
+ default: {
156
+ const _exhaustive: never = resolved;
157
+ throw new Error(
158
+ `Unexpected acp resolver reason: ${(_exhaustive as { reason: string }).reason}`,
159
+ );
160
+ }
161
+ }
162
+ }
163
+ const sendToClient = context.sendToClient as
164
+ | ((msg: { type: string; [key: string]: unknown }) => void)
165
+ | undefined;
121
166
  if (!sendToClient) {
122
167
  return {
123
168
  content: "No client connected - cannot spawn ACP agent.",
@@ -125,17 +170,6 @@ export async function executeAcpSpawn(
125
170
  };
126
171
  }
127
172
 
128
- // Resolve the agent, silently auto-installing a missing allowlisted
129
- // adapter binary (see acp/auto-install.ts). Shared with the HTTP route.
130
- const { resolved, autoInstalledPackage, failureMessage } =
131
- await resolveAgentWithAutoInstall(agent);
132
- if (failureMessage) {
133
- return { content: failureMessage, isError: true };
134
- }
135
- if (!resolved.ok) {
136
- return { content: formatResolveFailure(agent, resolved), isError: true };
137
- }
138
-
139
173
  // Inject required env vars and preflight via the shared helper. Mirrors
140
174
  // the HTTP route at `runtime/routes/acp-routes.ts:spawnSession` — both
141
175
  // call sites MUST go through `prepareAgentEnv` before `manager.spawn`,
@@ -151,12 +185,8 @@ export async function executeAcpSpawn(
151
185
  }
152
186
 
153
187
  // Best-effort version check — never blocks the spawn. If outdated, we
154
- // append a non-blocking warning to the success payload. Skipped for
155
- // bunx-resolved agents: there is no global npm install to compare (bun
156
- // fetches the package on first use), and npm may not exist on the host.
157
- const versionInfo = runsViaBunx(agentConfig)
158
- ? null
159
- : await checkAdapterVersion(adapterCommandOf(agentConfig));
188
+ // append a non-blocking warning to the success payload.
189
+ const versionInfo = await checkAdapterVersion(agentConfig.command);
160
190
 
161
191
  try {
162
192
  const manager = getAcpSessionManager();
@@ -167,21 +197,17 @@ export async function executeAcpSpawn(
167
197
  task,
168
198
  cwd,
169
199
  context.conversationId,
170
- sendToClient,
200
+ sendToClient as (msg: unknown) => void,
171
201
  );
172
202
 
173
- // Claude Code-only resume hint; empty for other adapters. Keyed off the
174
- // canonical adapter command so it survives the bunx rewrite. See
175
- // acp/resume-hint.ts for the gating rationale.
176
- const hint = claudeResumeHint(
177
- adapterCommandOf(agentConfig),
178
- cwd,
179
- protocolSessionId,
180
- );
181
- const resumeHint = hint ? ` ${hint}` : "";
182
- const installNote = autoInstalledPackage
183
- ? ` Installed ${autoInstalledPackage} automatically.`
184
- : "";
203
+ // `claude --resume <id>` is Claude Code-specific (the claude-agent-acp
204
+ // adapter binary). Other adapters resume differently or not at all,
205
+ // so the hint is gated by the resolved binary, not the agent id —
206
+ // this stays correct when a user aliases an id to a different binary.
207
+ const resumeHint =
208
+ agentConfig.command === "claude-agent-acp"
209
+ ? ` To resume this session later, run: cd ${cwd} && claude --resume ${protocolSessionId}`
210
+ : "";
185
211
  const payload = JSON.stringify({
186
212
  acpSessionId,
187
213
  protocolSessionId,
@@ -190,8 +216,7 @@ export async function executeAcpSpawn(
190
216
  status: "running",
191
217
  message:
192
218
  `ACP agent "${agent}" spawned (session: ${protocolSessionId}). ` +
193
- `Results stream back via SSE. You will be notified when it completes.` +
194
- `${installNote}${resumeHint}`,
219
+ `Results stream back via SSE. You will be notified when it completes.${resumeHint}`,
195
220
  });
196
221
 
197
222
  let content = payload;