Files
claude-agent-acp/src/serve.ts
Ben Brandt f115b90161 refactor: move the v1 agent into src/v1 (#1562)
Stacked on #1561.

Moves the modules that only the v1 agent uses into `src/v1/` with `git
mv`, so the tree reads as `src/v1/` (v1 agent), `src/v2/` (v2 agent) and
the rest of `src/` (shared). No behavior change.

The v1-only set comes from the import graph: modules reachable from
`acp-agent.ts` whose every non-test importer is itself v1-only (entry
points `lib.ts`/`serve.ts` aside).

Moved:
- `acp-agent.ts`, `background-work.ts`, `goal-extension.ts`,
`turn-events.ts`
- `tool-calls/changed-meta-filter.ts` ->
`v1/tool-calls/changed-meta-filter.ts`
- `session-index/*` -> `v1/session-index/*`, except
`session-index/explicit-title.ts`, which stays because
`session-titles.ts` (shared with v2) imports it

Stay shared: `clear-context-coordinator.ts`, `native-subagents.ts` and
`acp-subagents.ts`. v2 and shared modules import all three.

- `src/acp-agent.ts` is now `export * from "./v1/acp-agent.js"`.
`dist/acp-agent.js` exports the same names, and `module-exports.test.ts`
passes unchanged. `lib.ts` and `serve.ts` import `v1/acp-agent.js`
directly.
- Imports in `src/` and tests, path mentions in comments, and AGENTS.md
are updated. Scripts, vitest config and docs referenced no moved path.
- History: `git log --follow src/v1/acp-agent.ts` follows the move.
Because `src/acp-agent.ts` still exists as the re-export, blame needs
`git blame -C`.

Test results:
- `npx tsc --noEmit`, `npm run check`, `npm run build`: pass
- `npx vitest run`: 3939 passed / 33 skipped, goldens unchanged
- `node scripts/build-binary.mjs && node scripts/smoke-binary.mjs`: pass

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-10 08:44:57 +01:00

74 lines
2.9 KiB
TypeScript

import * as v2 from "@agentclientprotocol/sdk/experimental/v2";
import { v1AgentApp, type ClaudeAcpAgent } from "./v1/acp-agent.js";
import type { Logger } from "./logger.js";
import { nodeToWebReadable, nodeToWebWritable } from "./utils.js";
import { v2AgentApp, type AgentHandle } from "./v2/agent.js";
/**
* The ACP versions that the adapter serves: `v1` or `v2` alone, or both with
* `auto`, where the client's `initialize` picks the version.
*/
export type AcpProtocols = "auto" | "v1" | "v2";
/**
* Reads `CLAUDE_AGENT_ACP_PROTOCOL`: unset, empty, or `auto` serves both
* versions, and `v1` or `v2` serves that version alone. Throws on any other
* value, so that a typo such as `CLAUDE_AGENT_ACP_PROTOCOL=1` fails at startup
* instead of silently serving both.
*/
export function acpProtocolsFromEnv(value: string | undefined): AcpProtocols {
if (value === undefined || value === "") return "auto";
if (value === "auto" || value === "v1" || value === "v2") return value;
throw new Error(
`CLAUDE_AGENT_ACP_PROTOCOL must be "auto", "v1", or "v2", not ${JSON.stringify(value)}`,
);
}
/** Serves ACP on stdio through {@link acpProtocolRouter}. */
export function serveAcp(
logger: Logger | undefined,
options: { protocols: AcpProtocols },
): { closed: Promise<void>; dispose(): Promise<void> } {
const output = nodeToWebWritable(process.stdout);
const input = nodeToWebReadable(process.stdin);
// The router opens the connection of the negotiated version only when it
// reads `initialize`, so a client that leaves before then leaves no agent.
let agent: AgentHandle | undefined;
const onAgent = (created: AgentHandle) => {
agent = created;
};
const { closed } = acpProtocolRouter(logger, onAgent, options.protocols, {
onV2Agent: onAgent,
}).connect(v2.ndJsonStream(output, input));
return {
closed,
dispose: async () => {
await agent?.dispose();
},
};
}
/**
* Routes a connection to the protocol version that the client negotiates in
* `initialize`. With `auto`, a client that requests version 2 or later gets
* the draft ACP v2, and every other client gets ACP v1. With `v1`, a v2
* client is negotiated down to v1. With `v2`, the router rejects the
* `initialize` of a v1 client as an unsupported protocol version. `onAgent`
* receives the agent of a v1 connection (`ClaudeAcpAgent`) once the router
* has opened it, and `onV2Agent` the agent of a v2 connection, which the
* v2 core (`src/v2/`) serves.
*/
export function acpProtocolRouter(
logger: Logger | undefined,
onAgent: (agent: ClaudeAcpAgent) => void,
protocols: AcpProtocols = "auto",
{ onV2Agent = () => {} }: { onV2Agent?: (agent: AgentHandle) => void } = {},
): v2.AgentProtocolRouter {
const router = v2.agentProtocolRouter();
if (protocols !== "v2") router.withV1(v1AgentApp(logger, onAgent));
if (protocols !== "v1") {
router.withV2(v2AgentApp(logger, onV2Agent));
}
return router;
}