import type { HostCommandRegistry } from '../../registry/command/index.js'; import type { ToolPresenter } from '../../registry/tool/presentation.js'; import type { MCPTransport } from '../../types/connector/mcp.js'; import type { SessionEvent } from '../../types/session/events.js'; import { type Logger } from '../../utils/logger.js'; import type { AcpClientFilesystem } from './filesystem.js'; import type { AcpPermissionAsker } from './permission.js'; /** * An agent-client protocol server over stdio. * * An editor extension or a CI orchestrator could previously do two things: * shell out to the CLI and scrape its output, or embed the SDK in its own * process. This is the third — a wire surface a peer written in any language * can drive. * * **The precedent in this tree is a warning, and this change answers it.** * `MCPServer` and `ServerStdioTransport` are both exported, and nothing * anywhere constructs an `MCPServer`: a complete protocol server with no * driver. So `packages/cli/src/commands/acp.ts` ships in the same change, * and a subprocess test spawns the real binary — the wire half alone is not * a deliverable. * * **stdout belongs to the protocol.** `ServerStdioTransport`'s own header * says it and this is the surface that pays for it: one stray `console.log` * anywhere in the process and a client reports malformed JSON with nothing * naming the culprit. This repository's logger writes to stderr, and a test * asserts zero non-JSON bytes on the child's stdout under info-level * logging. */ /** What the bridge needs from the runtime, taken as an interface. */ export interface AcpAgentGateway { /** * Run one prompt as one turn of the session, streaming its events, and * resolve with the stop reason. * * A session has one active turn at a time. A gateway whose session log * refuses the turn because another is active (in this process or another) * throws the `TurnInProgressError`; the bridge answers the prompt with * `INVALID_REQUEST` naming the active turn. * * Deliberately not `AgentManagerContract` itself. This bridge needs one * verb, and a session front end holding the whole manager could cancel * somebody else's task, spawn children, or drain a queue it does not own * — none of which a peer asked for. The CLI passes an adapter over * `sendMessage`/`cancel`. */ prompt(request: { readonly sessionId: string; readonly prompt: string; readonly cwd: string; readonly onEvent: (event: SessionEvent) => void; readonly signal: AbortSignal; /** * Ask the human in front of the client about a tool batch. * * Handed to the gateway rather than installed by it, so the ONE place * that knows how to reach the client is this bridge. A gateway that * built its own asker would be a second path to the same human, and * the one that forgot to latch `approve_all` would be it. */ readonly ask: AcpPermissionAsker; /** * The client's buffers, when it declared the capability. `undefined` * means disk — which is correct, and is what every non-editor peer * wants. */ readonly filesystem: AcpClientFilesystem | undefined; /** Turns to resume from, oldest first. Empty for a fresh session. */ readonly history: readonly unknown[]; }): Promise<{ readonly stopReason?: string; /** * The exact conversation to use for the next prompt, after the turn has * settled. Omit it when the gateway does not own durable history. */ readonly history?: readonly unknown[]; }>; /** * The turns a prior session left behind, for `session/load`. * * Optional: a gateway with no session store cannot resume, and saying so * by not implementing this is better than returning an empty history that * a client cannot tell apart from a session that really had no turns. * * Resolves `undefined` when the store has no session by that id, which * the bridge answers with `INVALID_PARAMS` naming the id. The id is the * client's and may be any string: a namzu session id, or one the gateway * maps to a session through the index's `acp` / `session` refs. */ load?(sessionId: string): Promise; } export interface AcpServerOptions { readonly transport: MCPTransport; readonly gateway: AcpAgentGateway; /** * The command surface, verbatim from the registry. * * Passed as the registry rather than as a list so `describe()` is called * per initialize: a host that registers a command after construction * still has it appear, and this module has no place to hard-code one. */ readonly commands: HostCommandRegistry; readonly presenter: ToolPresenter; readonly agentInfo: { readonly name: string; readonly version: string; }; /** * The id `session/new` answers with. Defaults to a new namzu session id * (UUIDv7). A host may return any string; the bridge treats it as opaque, * and injecting it also keeps a test off a random id. */ readonly newSessionId?: () => string; readonly log?: Logger; } export declare class ACPServer { private readonly options; private readonly log; private readonly sessions; /** IDs being loaded or created but not yet published. */ private readonly reservedSessionIds; private initialized; private stopped; private clientCapabilities; /** * The method table, authored INDEPENDENTLY of `ACP_METHODS`. * * Deriving it from the constant would make the drift test a tautology. * Two hand-written sets compared in both directions is the only shape * where "a handler nobody advertises" and "an advertised method with no * handler" are both catchable. */ private readonly handlers; /** Requests this side has out to the client, keyed by their id. */ private readonly pending; private requestSeq; constructor(options: AcpServerOptions); /** The method names this server answers. For the drift test. */ methodNames(): readonly string[]; start(): Promise; stop(): Promise; /** * Ask the client something and wait for its answer. * * The direction this bridge did not have. A notification is fire and * forget; a permission prompt is a question the turn cannot proceed past, * so it needs an id, a place to park the promise, and a `dispatch` that * recognises a response frame. */ private request; private dispatch; private respondError; private send; private notifyUpdate; private onInitialize; private onSessionNew; /** * Resume a session the store already has. * * The prior turns come from the gateway, never from this bridge: the * history lives in whatever session store the host wired up, and a bridge * that kept its own copy would answer a resume with the turns THIS process * happened to see rather than the ones the session actually had. */ private onSessionLoad; private requireAbsoluteCwd; private reserveGeneratedSessionId; private reserveSessionId; private releaseSessionId; private publishSession; private requireInitialized; private requirePermissionCapability; private onSessionPrompt; private onSessionCancel; /** * Put a tool batch in front of the human, unless they already said yes to * everything for this session. */ private askPermission; /** * The client's buffers, when it declared the capability. * * `undefined` otherwise, and that is the ordinary case: a peer that is not * an editor has no buffers, and the agent should read the disk. */ private clientFilesystem; private requireSession; } //# sourceMappingURL=server.d.ts.map