ACP
connectAgent drives one Agent session over ACP.
Launch profiles
@rivus/agent-kit/acp exports connectAgent, probeAgent, agentEnv, and SessionBindingStore and its Layers. It runs with effect and a platform that spawns.
/acp has a launch profile for Claude Code (claude-agent-acp), Codex (codex-acp), DeepSeek Harness (dsh --profile acp), Gemini CLI, Grok and opencode. A profile's warnings name the facts not yet checked against the Agent. Cursor and Pi have no supported ACP transport. The generated support flags are on Supported Agents.
DeepSeek Harness resolves credentials in the order .credentials.yaml, inherited environment, <cwd>/.env, then $DSH_HOME/.env; custom pi-ai apiKeyEnv names are not in the built-in agentEnv filter, so explicitly add a referenced key to the launch environment only if it exists solely in the caller’s environment.
Sessions and turns
connectAgent starts an Agent's ACP program with exactly the environment you pass, completes the handshake and returns a connection that belongs to your Scope. Closing the Scope closes its sessions and stops the process.
A session runs one turn at a time, and prompt streams the turn. Deltas are named after AI SDK's stream parts (text-delta, reasoning-delta, tool-input-*, tool-output-partial, tool-output-available, tool-output-error, finish) and event parts carrying the completed TranscriptEvents. The turn opens with the updates that arrived while no turn ran, so a background call's result that lands between turns reaches its parts.
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Stream from "effect/Stream";
import { agentEnv, connectAgent, MemorySessionBindingStoreLive } from "@rivus/agent-kit/acp";
import { NodePlatformLive } from "@rivus/agent-kit/node/effect";
import { builtinProviders } from "@rivus/agent-kit/providers";
const profile = builtinProviders.find((provider) => provider.id === "claude-code")?.acp;
if (!profile) throw new Error("Claude Code ACP is unavailable");
const turn = Effect.scoped(
Effect.gen(function* () {
const connection = yield* connectAgent("claude-code", {
cwd: "/work/project",
// Only the variables the agent reads; nothing is inherited.
env: agentEnv(profile, process.env),
onPermission: (request) => Effect.succeed(askTheUser(request))
});
const session = yield* connection.newSession({ sessionKey: "chat:42", systemPrompt: "Be brief." });
yield* session.prompt([{ type: "text", text: "Summarize README.md" }]).pipe(Stream.runForEach(render));
})
);
await Effect.runPromiseExit(turn.pipe(Effect.provide(Layer.mergeAll(NodePlatformLive, MemorySessionBindingStoreLive))));Permission requests go to onPermission. Without it, or without an answer, they are denied. A callback whose policy answers cancelled returns { outcome: "cancelled" }, which the agent receives as the cancelled outcome instead of a denial.
The agent's stderr reaches onStderr as decoded text arrives, in order, and the tail already attached to errors stays. The callback is neither awaited nor let to fail the connection.
Interrupting a turn's stream, or session.cancel(), sends session/cancel and waits for the turn to end within cancelTimeoutMs (5 s by default). When it does not end in time, the session's binding is removed and the connection is closed, which fails its other sessions with ConnectionClosed; unsettledCancel: "session" closes only that session instead, with session/close when the agent supports it, and keeps the others running. Stopping the connection's process sends SIGTERM first and SIGKILL after killGraceMs (2 s by default), which a host that terminates on its own schedule sets shorter.
sessionKey binds a key to the session in the SessionBindingStore (MemorySessionBindingStoreLive or FileSessionBindingStoreLive(path)), so a later connection can loadSession({ sessionKey }). A session already open on the connection is returned as is; session.detach() forgets it locally instead, so the next loadSession re-sends session/load — with the MCP servers of that call, and no session/close to an agent that may not allow loading after one.
Client file reads and writes are off unless fileSystem turns them on, and then stay inside the session's directory after links are resolved.
probeAgent opens one trial session and reports ready, needs-login or unavailable.
Effect failures
/acp fails Effects with _tag errors:
AgentUnavailableandHandshakeFailedfromconnectAgentAuthRequired,AcpTimeout,AcpRequestFailed,ConnectionClosed,BindingNotFound,LoadUnsupportedandSessionBindingStoreFailurefrom opening sessionsTurnInProgress,SessionClosedandCancelUnsettledfrom turns
The platform Layer is Effect entries.