@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.
- package/bun.lock +2 -2
- package/docs/plugins.md +832 -0
- package/examples/plugins/echo/README.md +60 -61
- package/examples/plugins/echo/package.json +2 -1
- package/examples/plugins/echo/register.ts +143 -0
- package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +6 -7
- package/openapi.yaml +5 -15
- package/package.json +2 -2
- package/src/__tests__/agent-loop-exit-reason.test.ts +56 -3
- package/src/__tests__/anthropic-provider.test.ts +1 -1
- package/src/__tests__/app-control-flow.test.ts +1 -1
- package/src/__tests__/app-dir-path-guard.test.ts +0 -1
- package/src/__tests__/approval-routes-http.test.ts +1 -4
- package/src/__tests__/channel-approval-routes.test.ts +1 -1
- package/src/__tests__/channel-approvals.test.ts +1 -1
- package/src/__tests__/circuit-breaker-pipeline.test.ts +405 -0
- package/src/__tests__/compaction-pipeline.test.ts +210 -0
- package/src/__tests__/compaction-timeout-recovery.test.ts +251 -0
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +3 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +3 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +7 -3
- package/src/__tests__/conversation-agent-loop.test.ts +39 -42
- package/src/__tests__/conversation-clean-command.test.ts +2 -5
- package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -4
- package/src/__tests__/conversation-runtime-assembly.test.ts +71 -140
- package/src/__tests__/conversation-runtime-workspace.test.ts +27 -108
- package/src/__tests__/conversation-starter-routes.test.ts +6 -14
- package/src/__tests__/conversation-workspace-cache-state.test.ts +16 -17
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -61
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -7
- package/src/__tests__/db-acp-history.test.ts +0 -101
- package/src/__tests__/dynamic-page-surface.test.ts +0 -31
- package/src/__tests__/file-write-tool.test.ts +0 -63
- package/src/__tests__/gateway-only-guard.test.ts +2 -12
- package/src/__tests__/guardian-grant-minting.test.ts +1 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +4 -2
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +0 -1
- package/src/__tests__/heartbeat-service.test.ts +0 -1
- package/src/__tests__/host-app-control-routes.test.ts +1 -1
- package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
- package/src/__tests__/injector-background-turn.test.ts +1 -1
- package/src/__tests__/injector-chain.test.ts +6 -34
- package/src/__tests__/injector-disk-pressure.test.ts +34 -77
- package/src/__tests__/injector-document-comments.test.ts +1 -1
- package/src/__tests__/list-messages-hidden-metadata.test.ts +0 -38
- package/src/__tests__/memory-v2-static-injector.test.ts +1 -1
- package/src/__tests__/{overflow-reduction-loop.test.ts → overflow-reduce-pipeline.test.ts} +284 -64
- package/src/__tests__/pipeline-runner.test.ts +554 -0
- package/src/__tests__/plugin-api-shim.test.ts +6 -3
- package/src/__tests__/plugin-bootstrap.test.ts +23 -12
- package/src/__tests__/plugin-registry.test.ts +49 -3
- package/src/__tests__/plugin-types.test.ts +70 -0
- package/src/__tests__/reaction-persistence.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +1 -4
- package/src/__tests__/skill-feature-flags-integration.test.ts +0 -33
- package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
- package/src/__tests__/subagent-fork-notifications.test.ts +3 -1
- package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
- package/src/__tests__/subagent-manager-notify.test.ts +3 -1
- package/src/__tests__/subagent-notify-parent.test.ts +3 -1
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
- package/src/__tests__/user-plugin-loader.test.ts +286 -54
- package/src/acp/__tests__/client-handler.test.ts +0 -40
- package/src/acp/__tests__/prepare-agent-env.test.ts +0 -137
- package/src/acp/__tests__/session-manager-persistence.test.ts +28 -95
- package/src/acp/agent-process.ts +1 -61
- package/src/acp/client-handler.ts +0 -31
- package/src/acp/prepare-agent-env.ts +29 -83
- package/src/acp/resolve-agent.test.ts +7 -320
- package/src/acp/resolve-agent.ts +18 -182
- package/src/acp/session-manager.ts +73 -495
- package/src/acp/types.ts +0 -8
- package/src/agent/compaction-circuit.ts +102 -60
- package/src/agent/loop.ts +59 -32
- package/src/api/responses/conversation-message.ts +1 -7
- package/src/approvals/guardian-request-resolvers.ts +1 -1
- package/src/background-wake/next-wake.ts +0 -1
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
- package/src/config/acp-defaults.test.ts +0 -10
- package/src/config/acp-defaults.ts +0 -6
- package/src/config/bundled-skills/acp/SKILL.md +31 -83
- package/src/config/bundled-skills/acp/TOOLS.json +4 -4
- package/src/config/bundled-skills/app-builder/SKILL.md +381 -224
- package/src/config/bundled-skills/app-builder/TOOLS.json +0 -29
- package/src/config/bundled-skills/document-editor/SKILL.md +23 -28
- package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
- package/src/config/bundled-tool-registry.ts +0 -2
- package/src/config/feature-flag-registry.json +5 -14
- package/src/config/schemas/heartbeat.ts +0 -9
- package/src/context/strip-injections.ts +2 -8
- package/src/context/window-manager.ts +1 -2
- package/src/daemon/conversation-agent-loop-handlers.ts +11 -0
- package/src/daemon/conversation-agent-loop.ts +279 -62
- package/src/daemon/conversation-runtime-assembly.ts +69 -106
- package/src/daemon/conversation-store.ts +90 -9
- package/src/daemon/conversation-workspace.ts +0 -17
- package/src/daemon/conversation.ts +6 -0
- package/src/daemon/external-plugins-bootstrap.ts +11 -11
- package/src/daemon/handlers/conversations.ts +1 -3
- package/src/daemon/handlers/skills.ts +1 -4
- package/src/daemon/lifecycle.ts +0 -21
- package/src/daemon/server.ts +0 -2
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -3
- package/src/heartbeat/heartbeat-run-store.ts +1 -23
- package/src/heartbeat/heartbeat-service.ts +0 -26
- package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
- package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
- package/src/ipc/skill-routes/__tests__/memory.test.ts +0 -15
- package/src/ipc/skill-routes/memory.ts +2 -4
- package/src/memory/conversation-starter-checkpoints.ts +0 -1
- package/src/memory/db-init.ts +0 -2
- package/src/memory/job-handlers/conversation-starters.ts +2 -13
- package/src/memory/jobs-worker.ts +1 -1
- package/src/memory/migrations/index.ts +0 -1
- package/src/memory/schema/acp.ts +0 -4
- package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
- package/src/memory/v2/consolidation-job.ts +4 -13
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/assign.test.ts +4 -4
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/live-integration.test.ts +4 -4
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/maintain-job.test.ts +5 -5
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/orchestrate.test.ts +3 -3
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/reconcile.test.ts +2 -2
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/render-injection.test.ts +1 -1
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/router.test.ts +3 -3
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/selection-log-store.test.ts +8 -8
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/selector.test.ts +3 -3
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/shadow-plugin.test.ts +12 -12
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/assign.ts +5 -5
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/capabilities.ts +2 -2
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/maintain-job.ts +8 -8
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/page-content.ts +2 -2
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/provider-blocks.ts +1 -1
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/reconcile.ts +3 -3
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/render-injection.ts +1 -1
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/router.ts +3 -3
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/selection-log-store.ts +4 -4
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/selector.ts +4 -4
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/shadow-plugin.ts +90 -28
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/tree.ts +1 -1
- package/src/plugin-api/index.ts +5 -0
- package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +93 -0
- package/src/plugins/defaults/{memory-v3-shadow → circuit-breaker}/package.json +2 -2
- package/src/plugins/defaults/circuit-breaker/register.ts +39 -0
- package/src/plugins/defaults/compaction/middlewares/compaction.ts +25 -0
- package/src/plugins/defaults/compaction/package.json +1 -1
- package/src/plugins/defaults/compaction/register.ts +19 -8
- package/src/plugins/defaults/compaction/terminal.ts +73 -0
- package/src/plugins/defaults/index.ts +5 -3
- package/src/plugins/defaults/{memory-retrieval/injectors.ts → injectors/register.ts} +7 -45
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +7 -11
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
- package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +126 -0
- package/src/plugins/defaults/overflow-reduce/package.json +15 -0
- package/src/plugins/defaults/overflow-reduce/register.ts +42 -0
- package/src/plugins/external-api.ts +2 -2
- package/src/plugins/pipeline.ts +293 -6
- package/src/plugins/registry.ts +37 -9
- package/src/plugins/types.ts +336 -32
- package/src/plugins/user-loader.ts +127 -30
- package/src/proactive-artifact/aux-message-injector.ts +1 -1
- package/src/proactive-artifact/job.test.ts +1 -1
- package/src/prompts/__tests__/system-prompt.test.ts +0 -6
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +2 -4
- package/src/runtime/__tests__/agent-wake.test.ts +5 -5
- package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
- package/src/runtime/agent-wake.ts +3 -0
- package/src/runtime/assistant-event-hub.ts +1 -1
- package/src/runtime/channel-approvals.ts +1 -1
- package/src/runtime/interactive-ui.ts +1 -1
- package/src/runtime/routes/__tests__/acp-routes.test.ts +55 -283
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +4 -5
- package/src/runtime/routes/__tests__/surface-content-routes.test.ts +1 -4
- package/src/runtime/routes/acp-routes.test.ts +25 -89
- package/src/runtime/routes/acp-routes.ts +29 -81
- package/src/runtime/routes/approval-routes.ts +1 -1
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/browser-tabs-routes.ts +10 -6
- package/src/runtime/routes/conversation-cli-routes.ts +1 -1
- package/src/runtime/routes/conversation-list-routes.ts +1 -1
- package/src/runtime/routes/conversation-query-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +2 -15
- package/src/runtime/routes/conversation-starter-routes.ts +7 -13
- package/src/runtime/routes/conversations-import-routes.ts +7 -24
- package/src/runtime/routes/host-app-control-routes.ts +1 -1
- package/src/runtime/routes/host-cu-routes.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +3 -18
- package/src/runtime/routes/inbound-message-handler.ts +1 -1
- package/src/runtime/routes/memory-v3-routes.ts +6 -16
- package/src/runtime/routes/playground/helpers.ts +1 -1
- package/src/runtime/routes/surface-conversation-resolver.ts +3 -4
- package/src/runtime/routes/work-items-routes.ts +4 -2
- package/src/runtime/services/conversation-serializer.ts +1 -1
- package/src/signals/cancel.ts +4 -2
- package/src/subagent/manager.ts +5 -17
- package/src/tools/acp/list-agents.test.ts +1 -7
- package/src/tools/acp/spawn.test.ts +55 -158
- package/src/tools/acp/spawn.ts +72 -47
- package/src/tools/acp/steer.test.ts +8 -105
- package/src/tools/acp/steer.ts +17 -48
- package/src/tools/apps/executors.ts +8 -13
- package/src/tools/filesystem/write.ts +0 -34
- package/src/tools/subagent/spawn.ts +4 -2
- package/src/tools/ui-surface/definitions.ts +4 -25
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +5 -4
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +45 -69
- package/examples/plugins/echo/hooks/post-tool-use.ts +0 -18
- package/examples/plugins/echo/hooks/stop.ts +0 -16
- package/examples/plugins/echo/hooks/user-prompt-submit.ts +0 -18
- package/examples/plugins/echo/src/emit.ts +0 -19
- package/src/__tests__/compaction-circuit.test.ts +0 -258
- package/src/__tests__/compaction-direct.test.ts +0 -132
- package/src/__tests__/conversations-import-system-filter.test.ts +0 -101
- package/src/acp/__tests__/agent-process.test.ts +0 -161
- package/src/acp/__tests__/helpers/acp-history-db.ts +0 -82
- package/src/acp/__tests__/helpers/exec-file-stub.ts +0 -101
- package/src/acp/__tests__/session-manager-resume.test.ts +0 -736
- package/src/acp/auto-install.test.ts +0 -196
- package/src/acp/auto-install.ts +0 -177
- package/src/acp/feature-gate.test.ts +0 -48
- package/src/acp/feature-gate.ts +0 -34
- package/src/acp/resume-hint.ts +0 -25
- package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +0 -48
- package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +0 -57
- package/src/config/bundled-skills/app-builder/references/SLIDES.md +0 -38
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -62
- package/src/daemon/conversation-registry.ts +0 -159
- package/src/daemon/overflow-reduction-loop.ts +0 -230
- package/src/memory/migrations/272-acp-session-history-cwd.ts +0 -36
- package/src/plugins/defaults/compaction/compact.ts +0 -59
- package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +0 -14
- package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +0 -19
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +0 -75
- package/src/plugins/defaults/memory-v3-shadow/register.ts +0 -26
- package/src/tools/acp/context.ts +0 -20
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/capabilities.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/core.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/fixtures/eval-turns.json +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/fixtures/live-turns.json +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/health.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/needle.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/provider-blocks.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/snapshot.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/tree.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/types.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/working-set-eviction.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/working-set-skeleton.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/core.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/README.md +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/assignments.json +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/core.json +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-a/topic-x.md +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-a/topic-y.md +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-b/topic-z.md +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/health.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/llm-retry.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/needle.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/orchestrate.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/snapshot.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/types.ts +0 -0
- /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 {
|
|
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
|
-
*
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
22
|
-
* `
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
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
|
|
96
|
-
return
|
|
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
|
|
22
|
-
import {
|
|
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(
|
|
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
|
-
*
|
|
693
|
-
* re-applies injections via the supplied hooks.
|
|
694
|
-
* continue from, or `null` when the compactor
|
|
695
|
-
*
|
|
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
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
"
|
|
716
|
-
|
|
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
|
-
//
|
|
720
|
-
//
|
|
721
|
-
//
|
|
722
|
-
|
|
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-
|
|
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"
|
|
89
|
-
violations.push(`${prefix}: missing or
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
25
|
+
When the user first tries to use ACP and it's not configured, set it up automatically:
|
|
30
26
|
|
|
31
|
-
1. **
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
117
|
-
- NEVER use `claude`, `claude -p`, `claude --acp`,
|
|
118
|
-
- Default profiles for
|
|
119
|
-
- NEVER change an existing ACP config to use a different command. If the config already has `claude-agent-acp
|
|
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
|
|
72
|
+
## Updating the adapter
|
|
122
73
|
|
|
123
|
-
|
|
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
|
-
-
|
|
138
|
-
- **
|
|
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
|
|
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
|
|