@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
package/src/acp/types.ts CHANGED
@@ -12,14 +12,6 @@ export interface AcpAgentConfig {
12
12
  args: string[];
13
13
  description?: string;
14
14
  env?: Record<string, string>;
15
- /**
16
- * Canonical adapter identity (e.g. "claude-agent-acp"), set by the
17
- * resolver. It survives the bunx rewrite, where `command` becomes "bun"
18
- * and the adapter package moves into `args`. Optional because plain user
19
- * configs that bypass the resolver never set it; consumers fall back to
20
- * the command basename via `adapterCommandOf` in `resolve-agent.ts`.
21
- */
22
- adapterCommand?: string;
23
15
  }
24
16
 
25
17
  /**
@@ -1,15 +1,27 @@
1
- import type { CompactionCircuitEvent } from "../plugins/types.js";
1
+ import type { TrustContext } from "../daemon/trust-context.js";
2
+ import { FALLBACK_TURN_TRUST } from "../daemon/trust-context.js";
3
+ import { DEFAULT_TIMEOUTS, runPipeline } from "../plugins/pipeline.js";
4
+ import { getMiddlewaresFor } from "../plugins/registry.js";
5
+ import type {
6
+ CircuitBreakerArgs,
7
+ CircuitBreakerResult,
8
+ CompactionCircuitEvent,
9
+ TurnContext,
10
+ } from "../plugins/types.js";
2
11
 
3
12
  /**
4
- * Consecutive summary-LLM failures required to trip the breaker.
13
+ * Turn-scoped identifiers the circuit pipeline runner needs to populate its
14
+ * log records and plugin attribution. They are supplied per call because they
15
+ * belong to the active turn, not to the circuit's durable state — non-turn
16
+ * callers (e.g. `Conversation.forceCompact`) leave the optional fields unset
17
+ * and fall back to stable placeholders.
5
18
  */
6
- export const COMPACTION_CIRCUIT_FAILURE_THRESHOLD = 3;
7
-
8
- /**
9
- * Cooldown window after the breaker trips, during which auto-compaction is
10
- * suspended.
11
- */
12
- export const COMPACTION_CIRCUIT_COOLDOWN_MS = 60 * 60 * 1000;
19
+ export interface CircuitTurnInfo {
20
+ currentRequestId?: string;
21
+ currentTurnTrustContext?: TrustContext;
22
+ trustContext?: TrustContext;
23
+ turnCount: number;
24
+ }
13
25
 
14
26
  /**
15
27
  * Per-conversation compaction circuit breaker: three consecutive summary-LLM
@@ -18,10 +30,12 @@ export const COMPACTION_CIRCUIT_COOLDOWN_MS = 60 * 60 * 1000;
18
30
  * resets on process restart, the intended "one free retry after restart"
19
31
  * behavior.
20
32
  *
21
- * The dev-only playground routes (`POST /playground/reset-compaction-circuit`,
22
- * `POST /playground/inject-compaction-failures`) read and mutate
23
- * `consecutiveCompactionFailures` and `compactionCircuitOpenUntil` directly on
24
- * this object.
33
+ * The breaker's threshold/cooldown semantics live in the `circuitBreaker`
34
+ * plugin (`plugins/defaults/circuit-breaker/register.ts`), which mutates this
35
+ * object in place via the `CircuitBreakerArgs.state` container. This class is
36
+ * that container plus the methods the rest of the daemon uses to query and
37
+ * update it, so the dev-only playground routes can read and write the same
38
+ * fields directly.
25
39
  */
26
40
  export class CompactionCircuit {
27
41
  readonly conversationId: string;
@@ -37,62 +51,90 @@ export class CompactionCircuit {
37
51
  * transition event. Callers must only invoke this when the summary LLM
38
52
  * actually ran (`summaryFailed !== undefined`) so early-return paths don't
39
53
  * silently reset the 3-strike counter.
40
- *
41
- * A run of three failures trips the breaker; any success resets both the
42
- * counter and the cooldown timestamp. `compaction_circuit_open` fires once
43
- * when the counter first reaches the threshold while the circuit is dormant;
44
- * `compaction_circuit_closed` fires only on the open→closed transition.
45
54
  */
46
55
  async recordOutcome(
56
+ turn: CircuitTurnInfo,
47
57
  summaryFailed: boolean,
48
58
  onEvent: (msg: CompactionCircuitEvent) => void,
49
59
  ): Promise<void> {
50
- if (summaryFailed) {
51
- this.consecutiveCompactionFailures += 1;
52
- // Treat a stale/expired open-until timestamp the same as null so a new
53
- // 3-strike window can re-open the circuit after the prior cooldown
54
- // elapses. Without this, subsequent trips would no-op because
55
- // `compactionCircuitOpenUntil` remains set to a past timestamp even
56
- // though the breaker is effectively closed.
57
- const circuitDormant =
58
- this.compactionCircuitOpenUntil === null ||
59
- Date.now() >= this.compactionCircuitOpenUntil;
60
- if (
61
- this.consecutiveCompactionFailures >=
62
- COMPACTION_CIRCUIT_FAILURE_THRESHOLD &&
63
- circuitDormant
64
- ) {
65
- const openUntil = Date.now() + COMPACTION_CIRCUIT_COOLDOWN_MS;
66
- this.compactionCircuitOpenUntil = openUntil;
67
- onEvent({
68
- type: "compaction_circuit_open",
69
- conversationId: this.conversationId,
70
- reason: "3_consecutive_failures",
71
- openUntil,
72
- });
73
- }
74
- } else {
75
- // Emit only on the open→closed transition; firing on the common
76
- // closed→closed case would be noise.
77
- const wasOpen = this.compactionCircuitOpenUntil !== null;
78
- this.consecutiveCompactionFailures = 0;
79
- this.compactionCircuitOpenUntil = null;
80
- if (wasOpen) {
81
- onEvent({
82
- type: "compaction_circuit_closed",
83
- conversationId: this.conversationId,
84
- });
85
- }
86
- }
60
+ await this.run(turn, {
61
+ outcome: summaryFailed ? "failure" : "success",
62
+ onEvent,
63
+ });
87
64
  }
88
65
 
89
66
  /**
90
67
  * Query-only: is the breaker currently open? Auto-compaction paths gate on
91
- * `!isOpen()`; forced paths admit regardless. An expired open-until
92
- * timestamp reads as closed — it is the only source of truth for the gate.
68
+ * `!isOpen(...)`; forced paths admit regardless of the decision.
93
69
  */
94
- async isOpen(): Promise<boolean> {
95
- const openUntil = this.compactionCircuitOpenUntil;
96
- return openUntil !== null && Date.now() < openUntil;
70
+ async isOpen(turn: CircuitTurnInfo): Promise<boolean> {
71
+ const decision = await this.run(turn, {});
72
+ return decision.open;
73
+ }
74
+
75
+ /**
76
+ * Clear the breaker: zero the failure counter and close the circuit,
77
+ * emitting `compaction_circuit_closed` on the open→closed transition.
78
+ * Equivalent to recording a successful outcome.
79
+ */
80
+ async reset(
81
+ turn: CircuitTurnInfo,
82
+ onEvent?: (msg: CompactionCircuitEvent) => void,
83
+ ): Promise<void> {
84
+ await this.run(turn, {
85
+ outcome: "success",
86
+ ...(onEvent ? { onEvent } : {}),
87
+ });
88
+ }
89
+
90
+ private async run(
91
+ turn: CircuitTurnInfo,
92
+ args: {
93
+ outcome?: "success" | "failure";
94
+ onEvent?: (msg: CompactionCircuitEvent) => void;
95
+ },
96
+ ): Promise<CircuitBreakerResult> {
97
+ const turnContext = this.buildTurnContext(turn);
98
+ return runPipeline<CircuitBreakerArgs, CircuitBreakerResult>(
99
+ "circuitBreaker",
100
+ getMiddlewaresFor("circuitBreaker"),
101
+ async (terminalArgs) => {
102
+ // No plugin in the chain produced a decision. This should be
103
+ // unreachable in production because the default plugin registers a
104
+ // `circuitBreaker` middleware that always returns a decision, but we
105
+ // defensively derive the state here so test setups that intentionally
106
+ // omit the default plugin still get a sensible response.
107
+ const openUntil = terminalArgs.state.compactionCircuitOpenUntil;
108
+ const now = Date.now();
109
+ if (openUntil !== null && now < openUntil) {
110
+ return { open: true, cooldownRemainingMs: openUntil - now };
111
+ }
112
+ return { open: false };
113
+ },
114
+ {
115
+ key: `compaction:${this.conversationId}`,
116
+ // Pass this circuit as the mutable state container. The
117
+ // `CircuitBreakerArgs.state` shape matches its `conversationId` /
118
+ // `consecutiveCompactionFailures` / `compactionCircuitOpenUntil`
119
+ // fields so plugins mutate the same object the playground routes read.
120
+ state: this,
121
+ ...(args.outcome !== undefined ? { outcome: args.outcome } : {}),
122
+ ...(args.onEvent ? { onEvent: args.onEvent } : {}),
123
+ },
124
+ turnContext,
125
+ DEFAULT_TIMEOUTS.circuitBreaker,
126
+ );
127
+ }
128
+
129
+ private buildTurnContext(turn: CircuitTurnInfo): TurnContext {
130
+ return {
131
+ requestId: turn.currentRequestId ?? "circuit-breaker",
132
+ conversationId: this.conversationId,
133
+ turnIndex: turn.turnCount,
134
+ trust:
135
+ turn.currentTurnTrustContext ??
136
+ turn.trustContext ??
137
+ FALLBACK_TURN_TRUST,
138
+ };
97
139
  }
98
140
  }
package/src/agent/loop.ts CHANGED
@@ -12,14 +12,17 @@ import type { ContextWindowResult } from "../context/window-manager.js";
12
12
  import type { ToolActivityMetadata } from "../daemon/message-types/web-activity.js";
13
13
  import { HOOKS } from "../plugin-api/constants.js";
14
14
  import type { PostToolUseContext, StopContext } from "../plugin-api/types.js";
15
- import {
16
- DEFAULT_COMPACTION_PLUGIN_NAME,
17
- defaultCompact,
18
- } from "../plugins/defaults/compaction/compact.js";
15
+ import { defaultCompactionTerminal } from "../plugins/defaults/compaction/terminal.js";
19
16
  import type { PostCompactionHookInput } from "../plugins/defaults/memory-retrieval/hooks/post-compact.js";
20
- import { runHook } from "../plugins/pipeline.js";
21
- import type { CompactionCircuitEvent, TurnContext } from "../plugins/types.js";
22
- import { PluginExecutionError } from "../plugins/types.js";
17
+ import { DEFAULT_TIMEOUTS, runHook, runPipeline } from "../plugins/pipeline.js";
18
+ import { getMiddlewaresFor } from "../plugins/registry.js";
19
+ import type {
20
+ CompactionArgs,
21
+ CompactionCircuitEvent,
22
+ CompactionResult,
23
+ TurnContext,
24
+ } from "../plugins/types.js";
25
+ import { PluginTimeoutError } from "../plugins/types.js";
23
26
  import { normalizeThinkingConfigForWire } from "../providers/thinking-config.js";
24
27
  import type {
25
28
  ContentBlock,
@@ -454,7 +457,7 @@ export interface ResolvedSystemPrompt {
454
457
  /**
455
458
  * Orchestrator-supplied hook the loop invokes when the mid-loop budget gate
456
459
  * trips and inline compaction runs. The loop owns the trigger, the
457
- * compaction call, the result interpretation (circuit-breaker
460
+ * `compaction` pipeline call, the result interpretation (circuit-breaker
458
461
  * bookkeeping + the exhaustion decision), and the inline continue; this hook
459
462
  * bridges the injection state the loop is intentionally blind to. Durable
460
463
  * persistence is signalled out-of-band via the `history_stripped` (marker)
@@ -677,7 +680,15 @@ export class AgentLoop {
677
680
  onEvent: (event: AgentEvent) => void | Promise<void>,
678
681
  ): Promise<void> {
679
682
  try {
680
- await this.compactionCircuit.recordOutcome(summaryFailed, onEvent);
683
+ await this.compactionCircuit.recordOutcome(
684
+ {
685
+ currentRequestId: turnContext.requestId,
686
+ currentTurnTrustContext: turnContext.trust,
687
+ turnCount: turnContext.turnIndex,
688
+ },
689
+ summaryFailed,
690
+ onEvent,
691
+ );
681
692
  } catch (recordError) {
682
693
  log.error(
683
694
  { err: recordError, requestId: turnContext.requestId },
@@ -689,10 +700,11 @@ export class AgentLoop {
689
700
  /**
690
701
  * Compact the running history in place when the mid-loop budget gate trips.
691
702
  *
692
- * Calls the default compaction plugin on the stripped history, then
693
- * re-applies injections via the supplied hooks. Returns the history to
694
- * continue from, or `null` when the compactor exhausted its retry budget so
695
- * the caller yields `exitReason = "budget"` and the orchestrator escalates.
703
+ * Runs the `compaction` pipeline natively (like {@link estimateTokens}) on
704
+ * the stripped history, then re-applies injections via the supplied hooks.
705
+ * Returns the history to continue from, or `null` when the compactor timed
706
+ * out or exhausted its retry budget so the caller yields
707
+ * `exitReason = "budget"` and the orchestrator escalates.
696
708
  */
697
709
  private async compact(
698
710
  history: Message[],
@@ -709,27 +721,42 @@ export class AgentLoop {
709
721
  // Record the history-stripped marker right after stripping, before the
710
722
  // pipeline runs.
711
723
  await onEvent({ type: "history_stripped" });
712
- const manager = turnContext.contextWindowManager;
713
- if (manager == null) {
714
- throw new PluginExecutionError(
715
- "default-compaction: turnContext.contextWindowManager is missing — orchestrator must attach it before invoking compaction",
716
- DEFAULT_COMPACTION_PLUGIN_NAME,
724
+ let result: CompactionResult;
725
+ try {
726
+ result = await runPipeline<CompactionArgs, CompactionResult>(
727
+ "compaction",
728
+ getMiddlewaresFor("compaction"),
729
+ (args) => defaultCompactionTerminal(args, turnContext),
730
+ // The mid-loop budget gate is reached only when this turn decides to
731
+ // compact in place, so `force` the pipeline past its auto-threshold
732
+ // check. `actorTrustClass` comes from the turn context (the actor whose
733
+ // turn triggered compaction) so the compactor's image manifest excludes
734
+ // guardian-only attachments for untrusted actors. `overrideProfile` is
735
+ // the turn's resolved inference-profile override for the summary call.
736
+ {
737
+ messages: rawHistory,
738
+ signal,
739
+ options: {
740
+ force: true,
741
+ actorTrustClass: turnContext.trust.trustClass,
742
+ overrideProfile,
743
+ },
744
+ },
745
+ turnContext,
746
+ DEFAULT_TIMEOUTS.compaction,
717
747
  );
748
+ } catch (error) {
749
+ if (error instanceof PluginTimeoutError) {
750
+ // A timeout counts as a compaction failure against the circuit breaker.
751
+ await this.recordCompactionOutcome(turnContext, true, onEvent);
752
+ return null;
753
+ }
754
+ throw error;
718
755
  }
719
- // The mid-loop budget gate is reached only when this turn decides to
720
- // compact in place, so `force` past the auto-threshold check.
721
- // `actorTrustClass` comes from the turn context (the actor whose turn
722
- // triggered compaction) so the compactor's image manifest excludes
723
- // guardian-only attachments for untrusted actors. `overrideProfile` is the
724
- // turn's resolved inference-profile override for the summary call.
725
- const compactResult = await defaultCompact({
726
- manager,
727
- messages: rawHistory,
728
- signal,
729
- force: true,
730
- actorTrustClass: turnContext.trust.trustClass,
731
- overrideProfile,
732
- });
756
+ // `CompactionResult` is intentionally `unknown` at the plugin boundary so
757
+ // plugin consumers don't import the window manager; the loop ran the
758
+ // pipeline, so it interprets the concrete result here.
759
+ const compactResult = result as ContextWindowResult;
733
760
  // `force: true` bypasses the auto-threshold gate, but early returns
734
761
  // for "no eligible messages" / "insufficient messages" still leave
735
762
  // `summaryFailed` undefined. Only record an outcome when the summary LLM
@@ -335,13 +335,7 @@ export const ConversationMessageSchema = z.object({
335
335
  * assistant messages were consolidated for history rendering.
336
336
  */
337
337
  mergedMessageIds: z.array(z.string()).optional(),
338
- /**
339
- * Renderable conversation roles only. The messages endpoint emits a
340
- * UI-facing transcript, so it excludes the `system` rows that the
341
- * underlying `MessageRole` column also permits — those are agent-context
342
- * scaffolding, never a displayed turn.
343
- */
344
- role: z.enum(["user", "assistant"]),
338
+ role: z.string(),
345
339
  /**
346
340
  * Flat plain-text body (joined text segments). Redundant with
347
341
  * `textSegments`/`contentOrder` for clients that render from the positional
@@ -14,7 +14,7 @@
14
14
  import { answerCall } from "../calls/call-domain.js";
15
15
  import { findContactChannel } from "../contacts/contact-store.js";
16
16
  import { upsertContactChannel } from "../contacts/contacts-write.js";
17
- import { findConversation } from "../daemon/conversation-registry.js";
17
+ import { findConversation } from "../daemon/conversation-store.js";
18
18
  import {
19
19
  type CanonicalGuardianRequest,
20
20
  getCanonicalGuardianRequest,
@@ -97,7 +97,6 @@ function getHeartbeatWakeSource(now: number): HeartbeatWakeSource | null {
97
97
 
98
98
  const service = HeartbeatService.getInstance();
99
99
  if (service?.isConsecutiveRunCapReached) return null;
100
- if (service?.isDailyCapReached) return null;
101
100
 
102
101
  const serviceNextRunAt = service?.nextRunAt ?? null;
103
102
  let nextRunAt = serviceNextRunAt;
@@ -85,8 +85,8 @@ describe("unified feature flag registry guard", () => {
85
85
  ) {
86
86
  violations.push(`${prefix}: missing or non-string 'description'`);
87
87
  }
88
- if (typeof flag.defaultEnabled !== "boolean" && typeof flag.defaultEnabled !== "string") {
89
- violations.push(`${prefix}: missing or invalid 'defaultEnabled' (expected boolean or string)`);
88
+ if (typeof flag.defaultEnabled !== "boolean") {
89
+ violations.push(`${prefix}: missing or non-boolean 'defaultEnabled'`);
90
90
  }
91
91
  }
92
92
 
@@ -10,7 +10,6 @@ describe("DEFAULT_ACP_AGENT_PROFILES", () => {
10
10
  expect(Object.keys(DEFAULT_ACP_AGENT_PROFILES).sort()).toEqual([
11
11
  "claude",
12
12
  "codex",
13
- "gemini",
14
13
  ]);
15
14
  });
16
15
 
@@ -30,14 +29,6 @@ describe("DEFAULT_ACP_AGENT_PROFILES", () => {
30
29
  });
31
30
  });
32
31
 
33
- test("gemini profile speaks native ACP via gemini --acp (no adapter binary)", () => {
34
- expect(DEFAULT_ACP_AGENT_PROFILES.gemini).toEqual({
35
- command: "gemini",
36
- args: ["--acp"],
37
- description: "Google Gemini CLI (native ACP via gemini --acp)",
38
- });
39
- });
40
-
41
32
  test("is deeply frozen so mutation throws in strict mode", () => {
42
33
  expect(Object.isFrozen(DEFAULT_ACP_AGENT_PROFILES)).toBe(true);
43
34
  for (const profile of Object.values(DEFAULT_ACP_AGENT_PROFILES)) {
@@ -55,7 +46,6 @@ describe("DEFAULT_AGENT_NPM_PACKAGES", () => {
55
46
  expect(DEFAULT_AGENT_NPM_PACKAGES).toEqual({
56
47
  "claude-agent-acp": "@agentclientprotocol/claude-agent-acp",
57
48
  "codex-acp": "@zed-industries/codex-acp",
58
- gemini: "@google/gemini-cli",
59
49
  });
60
50
  });
61
51
 
@@ -30,11 +30,6 @@ export const DEFAULT_ACP_AGENT_PROFILES: Readonly<
30
30
  args: FROZEN_EMPTY_ARGS,
31
31
  description: "OpenAI Codex CLI (via @zed-industries/codex-acp)",
32
32
  }),
33
- gemini: Object.freeze({
34
- command: "gemini",
35
- args: Object.freeze(["--acp"]) as unknown as string[],
36
- description: "Google Gemini CLI (native ACP via gemini --acp)",
37
- }),
38
33
  });
39
34
 
40
35
  /**
@@ -49,5 +44,4 @@ export const DEFAULT_AGENT_NPM_PACKAGES: Readonly<Record<string, string>> =
49
44
  Object.freeze({
50
45
  "claude-agent-acp": "@agentclientprotocol/claude-agent-acp",
51
46
  "codex-acp": "@zed-industries/codex-acp",
52
- gemini: "@google/gemini-cli",
53
47
  });
@@ -7,30 +7,29 @@ metadata:
7
7
  vellum:
8
8
  display-name: "ACP"
9
9
  activation-hints:
10
- - "User asks to use Claude Code, Codex, or Gemini to do something"
11
- - "User wants to delegate a coding task to Claude Code, Codex, Gemini, or another ACP agent"
12
- - "User wants to hand a coding task to another agent and check on it later"
10
+ - "User wants to delegate a coding task to Claude Code, Codex, or another ACP agent"
13
11
  - "User wants to spawn an external coding agent that runs autonomously and streams results back"
14
- - "User mentions ACP, claude-agent-acp, codex-acp, gemini --acp, or running multiple coding agents in parallel"
12
+ - "User mentions ACP, claude-agent-acp, codex-acp, or running multiple coding agents in parallel"
15
13
  avoid-when:
16
- - "Task is small enough to do inline with the assistant's own tools - no need for an external agent"
14
+ - "Task is small enough to do inline with the assistant's own tools no need for an external agent"
17
15
  ---
18
16
 
19
- ACP agent orchestration - spawn external coding agents (Claude Code, Codex, Gemini) to work on tasks via the Agent Client Protocol. Each agent runs as its own subprocess speaking ACP over stdio and streams results back into the conversation.
17
+ ACP agent orchestration - spawn external coding agents (Claude Code, Codex, etc.) to work on tasks via the Agent Client Protocol. Each agent runs as its own subprocess speaking ACP over stdio and streams results back into the conversation.
20
18
 
21
19
  ## Usage
22
20
 
23
21
  Use `acp_spawn` to delegate a coding task to an external agent. The agent runs as a subprocess speaking the ACP protocol over stdio and streams results back.
24
22
 
25
- Users can refer to agents by natural names: "claude code", "codex cli", "openai codex", "gemini cli", and "google gemini" all resolve to the canonical `claude`, `codex`, and `gemini` ids (unless the user's config defines an agent literally keyed by that name, which always wins).
26
-
27
23
  ## First-time setup
28
24
 
29
- When the user first tries to use ACP and it's not enabled, set it up automatically:
25
+ When the user first tries to use ACP and it's not configured, set it up automatically:
30
26
 
31
- 1. **Enable the `acp` feature flag** (the primary enablement path). Either PATCH it via the gateway feature-flags endpoint or direct the user to toggle "ACP Coding Agents" in the client's feature flags UI. Flag changes are hot-refreshed in the assistant - no restart needed.
27
+ 1. **Check if `claude-agent-acp` is installed** by running `which claude-agent-acp`. If not found, install it:
28
+ ```bash
29
+ npm i -g @agentclientprotocol/claude-agent-acp
30
+ ```
32
31
 
33
- As a supported alternative, edit the workspace config file to add the `acp` section. Default profiles for `claude`, `codex`, and `gemini` ship out-of-box, so the minimal config is just:
32
+ 2. **Enable ACP in the workspace config** by editing the config file to add the `acp` section. Default profiles for `claude` and `codex` ship out-of-box, so the minimal config is just:
34
33
  ```json
35
34
  {
36
35
  "acp": {
@@ -39,108 +38,57 @@ When the user first tries to use ACP and it's not enabled, set it up automatical
39
38
  }
40
39
  }
41
40
  ```
42
- If you go the config route, **wait a few seconds** for the config watcher to pick up the change (it hot-reloads automatically - no restart needed).
43
-
44
- 2. Then retry the `acp_spawn` call. Do NOT run `vellum sleep && vellum wake` - that kills the conversation.
45
-
46
- No manual binary installation is needed first: missing adapter binaries are run on demand via bun, or installed automatically (see below).
47
-
48
- ## Automatic adapter availability
49
-
50
- When `acp_spawn` finds the agent's binary missing from PATH, the assistant runs the adapter through bun instead (`bun x --bun <package>`). Bun fetches the package into its cache on first use, so there is no global install and it works on hosts without node or npm (platform-hosted assistants ship bun only). When bun itself is not on PATH, the assistant falls back to silently installing the matching npm package globally and proceeds in the same call.
51
-
52
- Only the allowlisted out-of-box packages are ever run or installed this way (`@agentclientprotocol/claude-agent-acp`, `@zed-industries/codex-acp`, `@google/gemini-cli`); user-configured agents with custom commands are never fetched or installed automatically.
53
41
 
54
- Manual installation is fallback guidance for unusual setups: bun and npm both unavailable, restricted global installs, or an auto-install failure (the failure reason is surfaced in the tool result).
42
+ 3. **Wait a few seconds** for the config watcher to pick up the change (it hot-reloads automatically - no restart needed).
55
43
 
56
- ```bash
57
- npm i -g @agentclientprotocol/claude-agent-acp # claude
58
- npm i -g @zed-industries/codex-acp # codex
59
- npm i -g @google/gemini-cli # gemini
60
- ```
61
-
62
- ## Claude setup
63
-
64
- The `claude-agent-acp` adapter requires a Claude OAuth token. Store it once in the credential store and every spawn injects it as `CLAUDE_CODE_OAUTH_TOKEN` automatically:
65
-
66
- ```bash
67
- assistant credentials set --service acp --field claude_oauth_token <token>
68
- ```
69
-
70
- When the token is missing, do NOT ask the user to paste it into chat. Collect it via the secret-request flow instead: `credential_store` with action `prompt`, service `acp`, field `claude_oauth_token`. That prompts the user through a secure UI so the token never enters the conversation or the workspace config. Users generate the token by running `claude setup-token` on a machine where they are logged in to Claude.
44
+ 4. Then retry the `acp_spawn` call. Do NOT run `vellum sleep && vellum wake` - that kills the conversation.
71
45
 
72
46
  ## Codex setup
73
47
 
74
- The `codex-acp` adapter is fetched automatically when missing, but it shells out to the underlying `codex` CLI, which must also be on PATH:
48
+ To use Codex via ACP, both the `codex-acp` adapter and the underlying `codex` CLI must be on PATH:
49
+
50
+ 1. **Install the ACP adapter:**
51
+ ```bash
52
+ npm i -g @zed-industries/codex-acp
53
+ ```
54
+ This provides the `codex-acp` binary that the assistant spawns.
75
55
 
76
- 1. **Install the Codex CLI** (version 0.111 or higher) via OpenAI's distribution channel of choice. The adapter will fail if `codex` isn't on PATH.
56
+ 2. **Install the Codex CLI** (version 0.111 or higher) via OpenAI's distribution channel of choice. The `codex-acp` adapter shells out to `codex` under the hood and will fail if it isn't on PATH.
77
57
 
78
- 2. **Authenticate.** The `codex-acp` adapter inherits whatever auth the underlying `codex` CLI uses. Typical flows:
58
+ 3. **Authenticate.** The `codex-acp` adapter inherits whatever auth the underlying `codex` CLI uses. Typical flows:
79
59
  - `codex login` (OAuth)
80
60
  - `CODEX_API_KEY` environment variable
81
61
  - `OPENAI_API_KEY` environment variable
82
62
 
83
- ## Gemini setup
84
-
85
- Gemini CLI speaks ACP natively (`gemini --acp`) - there is no separate adapter binary. The CLI itself is fetched from `@google/gemini-cli` when missing.
86
-
87
- **Authenticate** with an API key through the credential store (the primary path):
88
-
89
- ```bash
90
- assistant credentials set --service acp --field gemini_api_key <key>
91
- ```
92
-
93
- Or collect the key via the secret-request flow: `credential_store` with action `prompt`, service `acp`, field `gemini_api_key`. Either way the key never appears in chat or workspace config, and every spawn injects it as `GEMINI_API_KEY` automatically. The key is optional - a spawn proceeds without it when the vault has no entry.
94
-
95
- The alternative is browser OAuth: run `gemini` once interactively and complete the sign-in flow. This is impractical on hosted assistants (no browser), so prefer the credential store there.
96
-
97
- Do NOT put API keys (or any secret) in the workspace config file - secrets never belong in the workspace directory. Use the credential store instead.
98
-
99
- A workspace `acp.agents.gemini` override is only for non-secret customization (custom binary path, extra args, non-secret env vars). It must spell out the full `command` and `args` - see "Critical: correct agent command" below for the replace-not-merge rule:
100
- ```json
101
- {
102
- "acp": {
103
- "agents": {
104
- "gemini": {
105
- "command": "gemini",
106
- "args": ["--acp"],
107
- "env": { "NO_COLOR": "1" }
108
- }
109
- }
110
- }
111
- }
112
- ```
63
+ If `codex-acp` isn't on PATH when the user asks for it, the assistant will surface the install hint via `acp_list_agents`.
113
64
 
114
65
  ## Critical: correct agent command
115
66
 
116
- - Three agents are supported out-of-box: `claude` (via the `claude-agent-acp` adapter), `codex` (via the `codex-acp` adapter), and `gemini` (via `gemini --acp` - Gemini speaks ACP natively, no adapter binary).
117
- - NEVER use `claude`, `claude -p`, `claude --acp`, or the bare `codex` CLI as the ACP `command`. Claude and Codex only speak the protocol through their dedicated `*-acp` adapters. Gemini is the exception: the `gemini` CLI itself speaks ACP when launched with `--acp`.
118
- - Default profiles for all three ship out-of-box. Users only need an `agents.<id>` entry in config if they want to override the defaults (e.g. point to a custom binary path or pass extra args/env). An `acp.agents.<id>` entry replaces the bundled default entirely (no field merge), so any override must spell out the full `command` and `args`, not just the field being changed.
119
- - NEVER change an existing ACP config to use a different command. If the config already has `claude-agent-acp`, `codex-acp`, or `gemini`, leave it alone.
67
+ - `claude-agent-acp` and `codex-acp` are the two supported adapter binaries today. They are what speak the ACP JSON-RPC protocol.
68
+ - NEVER use `claude`, `claude -p`, `claude --acp`, the bare `codex` CLI, or any other command as the ACP `command`. Only the dedicated `*-acp` adapters speak the protocol.
69
+ - Default profiles for `claude` and `codex` ship out-of-box. Users only need an `agents.<id>` entry in config if they want to override the defaults (e.g. point to a custom binary path or pass extra args).
70
+ - NEVER change an existing ACP config to use a different command. If the config already has `claude-agent-acp` or `codex-acp`, leave it alone.
120
71
 
121
- ## Updating an adapter
72
+ ## Updating the adapter
122
73
 
123
- This applies only to adapters installed globally via npm; adapters run via bun are fetched on first use and have no global install to update. If `acp_spawn` reports that an adapter is outdated, ask the user before updating. To update:
74
+ If `acp_spawn` reports that an adapter is outdated, ask the user before updating. To update:
124
75
 
125
76
  ```bash
126
77
  npm i -g @agentclientprotocol/claude-agent-acp@latest
127
78
  # or
128
79
  npm i -g @zed-industries/codex-acp@latest
129
- # or
130
- npm i -g @google/gemini-cli@latest
131
80
  ```
132
81
 
133
82
  Then retry the `acp_spawn` call.
134
83
 
135
84
  ## When to use acp_steer vs acp_spawn
136
85
 
137
- - **On a running session, `acp_steer` interrupts the in-flight prompt.** Use it to course-correct ("stop, do X instead"). It cancels whatever the agent is currently working on and replaces it with the new instruction. Queued follow-ups behind a running prompt are not supported - wait for the `acp_session_completed` notification instead.
138
- - **On a completed (or assistant-restarted) session, `acp_steer` transparently resumes it.** The session is restored from persisted history via ACP session loading when the agent supports it, and the new instruction runs with the agent's full prior context. This is the primary way to do follow-up work on an existing session id - prefer it over spawning a fresh session that would lose context.
139
- - If resume isn't possible (the session was recorded before resume support and has no working directory, or the agent lacks the capability), the error explains why; fall back to `acp_spawn`. For claude sessions, the completion message also includes a `claude --resume <id>` CLI hint for resuming outside the assistant.
86
+ - **`acp_steer` interrupts the in-flight prompt.** Use it to course-correct a running agent ("stop, do X instead"). It cancels whatever the agent is currently working on and replaces it with the new instruction.
87
+ - **For follow-ups after the current task** ("also do Y when you're done"), do NOT use `acp_steer`. Wait for the `acp_session_completed` notification and call `acp_spawn` again with the new task. Queued follow-ups in the same session are not yet supported.
140
88
 
141
89
  ## Discoverability
142
90
 
143
- Use `acp_list_agents` to see what's set up and what's missing. It returns each available agent profile, whether ACP is enabled, whether the agent is runnable (its binary is on PATH, or the assistant can run it via bun), and an install hint if not. This is the right tool to call when deciding between `claude`, `codex`, and `gemini`, or when the user asks "what coding agents do I have?"
91
+ Use `acp_list_agents` to see what's set up and what's missing. It returns each available agent profile, whether ACP is enabled, whether the agent's binary is on PATH, and an install hint if not. This is the right tool to call when deciding between `claude` and `codex`, or when the user asks "what coding agents do I have?"
144
92
 
145
93
  ## Working directory
146
94