import type { AnyMessage, AnyResponse, AnyWireMessage } from "./jsonrpc.js"; import type { WireStream } from "./stream.js"; /** Default for {@link ConnectionLimits.maxBufferedBytes}: 64 MiB. */ export declare const DEFAULT_MAX_BUFFERED_BYTES: number; /** Default for {@link ConnectionLimits.maxOutputStallMs}: 60 seconds. */ export declare const DEFAULT_MAX_OUTPUT_STALL_MS = 60000; /** Default for {@link ConnectionLimits.maxBufferedSessionStreams}: 1024. */ export declare const DEFAULT_MAX_BUFFERED_SESSION_STREAMS = 1024; /** Default for {@link ConnectionLimits.maxIdLength}: 1024. */ export declare const DEFAULT_MAX_ID_LENGTH = 1024; /** Per-connection limits on the state an ACP server keeps for a client. */ export interface ConnectionLimits { /** * Maximum agent output a connection holds for its client before the client * reads it. When a connection reaches the limit, it stops taking output * from the agent and holds back client requests until the client reads; * see `maxOutputStallMs` for a client that does not. Only an agent that * awaits its sends is slowed down, and output can pass the limit by a * message or two. * * Streamable HTTP connections count the output queued on all their * streams, including streams the client has not opened, by the length of * its JSON text in UTF-16 code units. Text outside Latin-1 takes twice that * in memory. Held-back requests stay open with their bodies, so limit how * many requests a client may have open in the HTTP server or proxy. * Requests the agent has accepted but not answered are not counted; bound * those in the agent. * * WebSocket connections count the bytes the socket reports as not yet sent * (`bufferedAmount`), and do not limit output if the socket does not report * it. They also close when client messages waiting to be handled exceed the * limit, each counted as its length plus 1 KiB, so keep the WebSocket * server's maximum message size below it. * Defaults to {@link DEFAULT_MAX_BUFFERED_BYTES}. */ readonly maxBufferedBytes?: number; /** * Maximum time, in milliseconds, a connection holding `maxBufferedBytes` * waits for its client to read any of it. The connection then closes, * which frees the agent, and client requests held back, from a client that * stopped reading or never opens the stream its output is for; memory * stays bounded meanwhile. On WebSocket connections, any bytes the socket * sends count as reading. At most 2^31 - 1 (about 24.8 days), the longest * a timer can wait. Defaults to {@link DEFAULT_MAX_OUTPUT_STALL_MS}. */ readonly maxOutputStallMs?: number; /** * Maximum sessions per Streamable HTTP connection holding messages that no * client is receiving, such as responses for a session whose stream was * never opened. The connection closes when agent output would buffer * another session, or when a client leaves a session stream with messages * still queued past the limit. Open session streams are not counted: each * is backed by an open request, so the embedding HTTP server or proxy must * limit how many a client may hold. WebSocket connections deliver * everything over the socket and never buffer per session. Defaults to * {@link DEFAULT_MAX_BUFFERED_SESSION_STREAMS}. */ readonly maxBufferedSessionStreams?: number; /** * Maximum length, in UTF-16 code units, of the session IDs and request IDs * a connection keeps track of. Client messages carrying a longer session ID * or request ID are rejected (HTTP 400, or WebSocket close code 1008), and * a connection whose agent issues a longer session ID is closed. Defaults * to {@link DEFAULT_MAX_ID_LENGTH}. */ readonly maxIdLength?: number; } /** Raised when a client or agent exceeds a {@link ConnectionLimits} limit. */ export declare class ConnectionLimitError extends Error { constructor(message: string); } /** Error for a connection that holds more than a limit allows. */ export declare function connectionLimitExceeded(limit: "maxBufferedBytes" | "maxBufferedSessionStreams", limits: Required): ConnectionLimitError; /** Error for a connection whose client read none of its output in time. */ export declare function connectionOutputStalled(limits: Required): ConnectionLimitError; /** Raised when a message is sent to a connection that has shut down. */ export declare class ConnectionClosedError extends Error { constructor(); } export interface AgentConnectOptions { readonly deferConnectHandlers?: boolean; } export interface AgentConnectionLifecycle { readonly closed?: Promise; startConnectHandlers?(): void; } export interface AgentConnector { connect(stream: WireStream, options?: AgentConnectOptions): AgentConnectionLifecycle | unknown; } export type ResponseRoute = "connection" | { readonly session: string; }; export interface OutboundLease { /** * Aborted when the stream is shut down while this lease holds it, with * the error as its reason or a `ConnectionClosedError` if there was none. * A receiver waiting on `receive()` learns this from its result; one that * is not, such as a response body whose client stopped reading, must stop * on this signal. */ readonly stopped: AbortSignal; receive(): Promise>; release(): void; } /** Follows what a stream queues, for its connection's accounting. */ export interface OutboundQueueObserver { /** Called as a message is queued. */ queued?(message: Message): void; /** Called as a message leaves the queue, taken by a receiver or dropped. */ dequeued(message: Message): void; } export declare class OutboundMailbox { private readonly enabled; private onReceiverChange?; private observer?; /** Queued messages start at `head`; see `dequeue`. */ private queue; private head; private activeLease; private isFinished; private isAborted; private abortError; constructor(enabled?: boolean, onReceiverChange?: (() => void) | undefined, observer?: OutboundQueueObserver | undefined); push(message: Message): void; get hasReceiver(): boolean; get hasQueuedMessages(): boolean; tryAcquire(): OutboundLease | undefined; finish(): void; abort(error?: unknown): void; /** * Takes the oldest queued message. `Array.prototype.shift` copies the rest * of a large array, which would make draining a full queue quadratic, so * this advances `head` instead and drops the taken prefix once the queue * empties or is mostly taken. */ private dequeue; } export type ConnectionTransport = "http" | "websocket"; export declare class ConnectionState { private readonly transport; private readonly limits; readonly connectionId: string; readonly inboundTx: WritableStream; readonly outboundRx: ReadableStream; /** Connection-level HTTP stream, queuing each message as JSON text. */ readonly connectionStream: OutboundMailbox; /** Every outbound message, for WebSocket connections. */ readonly allOutbound: OutboundMailbox; /** Session-level HTTP streams, queuing each message as JSON text. */ readonly sessionStreams: Map>; readonly pendingRoutes: Map; readonly clientResponseRoutes: Map; readonly closed: Promise; private readonly agentConnection; /** Length of the JSON text queued on HTTP streams; see `maxBufferedBytes`. */ private bufferedBytes; private readonly streamObserver; /** Session streams without a receiver; see `maxBufferedSessionStreams`. */ private bufferedSessionStreams; /** Called when a client takes output, or the connection stops routing. */ private readonly progressWaiters; private supportsBatches; private hasStartedRouter; private hasAgentOutputEnded; private hasFinishedRouting; private hasStartedShutdown; private inboundWriteChain; private initialReader; private outboundReader; private routerPromise; private shutdownPromise; private hasResolvedClosed; private abortAgentInbound; private endAgentOutbound; private resolveClosed; constructor(agent: AgentConnector, transport: ConnectionTransport, limits: Required); recvInitial(initializeId: string | number): Promise; writeInbound(message: AnyWireMessage): Promise; startRouter(): void; startConnectHandlers(): void; /** Enables JSON-RPC batch frames after ACP v2 is negotiated. */ enableBatches(): void; get batchesEnabled(): boolean; /** Whether shutdown has begun; a closed connection accepts no more work. */ get isClosed(): boolean; /** * Whether the connection takes no more agent output for now. HTTP * connections hold up to `maxBufferedBytes` in their streams. WebSocket * connections hand each message to the socket pump, which waits while the * socket holds `maxBufferedBytes`, so they wait until the pump takes it. */ get hasOutboundBacklog(): boolean; /** Throws `ConnectionLimitError` for a session ID over `maxIdLength`. */ validateSessionId(sessionId: string): void; /** Throws `ConnectionLimitError` for a request ID over `maxIdLength`. */ validateRequestId(id: unknown): void; /** * Resolves once the connection can take more agent output, or has stopped, * and rejects if `signal` aborts first. Client requests wait on this, so a * client that is not reading cannot make the agent produce more. */ waitForOutboundCapacity(signal?: AbortSignal): Promise; /** * Returns the stream that delivers messages for a session, creating one * that buffers them until a receiver attaches. Throws * `ConnectionLimitError` for an over-long ID or when no further session * may buffer. */ ensureSession(sessionId: string): OutboundMailbox; /** * Attaches a receiver to a session's stream, or returns `undefined` if the * stream cannot take one. The open request backing the receiver bounds it, * so this is not limited by `maxBufferedSessionStreams`. Throws * `ConnectionLimitError` for an over-long ID, and `ConnectionClosedError` * for a new stream once the connection can no longer route output to it. */ acquireSessionStream(sessionId: string): OutboundLease | undefined; shutdown(error?: unknown): Promise; private runShutdown; private observeAgentConnection; private cancelOutboundReader; private closeInbound; private writeInboundMessage; private runRouter; private resolveClosedOnce; /** * Waits while the connection holds `maxBufferedBytes` of output, until * `stopWaiting()` holds or the connection closes, and rejects if `signal` * aborts first. An HTTP client that takes none of the output for * `maxOutputStallMs`, having stopped reading or never opened the stream it * is for, is not coming back for it, so the connection closes rather than * keep the agent or the client's own requests waiting. A WebSocket session * times out its socket itself, where it can see each byte the client reads. */ private waitForRoom; /** * Resolves `true` the next time a client takes output or the connection * stops routing, or on HTTP, `false` if `maxOutputStallMs` passes first. * Rejects if `signal` aborts first, leaving nothing behind. */ private waitForProgress; private notifyOutboundProgress; private routeOutbound; private routeOutboundMessage; private routeOutboundResponse; private createSessionStream; /** * Keeps `bufferedSessionStreams` in step as receivers attach and leave. A * stream a receiver leaves is kept only while messages still wait on it. */ private onSessionReceiverChange; private closeForLimit; private validateIdLength; private routeOutboundRequestOrNotification; private trackClientResponseRoute; private pushToRoute; /** * Queues a message on an HTTP stream as JSON text, which is all a stream * delivers and all `maxBufferedBytes` needs to count. WebSocket * connections deliver through `allOutbound` instead. */ private deliver; } export declare class ConnectionRegistry { readonly limits: Required; private readonly connections; private readonly pendingConnections; constructor(options?: ConnectionLimits); createConnection(agent: AgentConnector, transport?: ConnectionTransport): ConnectionState; createPendingConnection(agent: AgentConnector, transport?: ConnectionTransport): ConnectionState; register(connection: ConnectionState): void; get(connectionId: string): ConnectionState | undefined; remove(connectionId: string): ConnectionState | undefined; discard(connectionId: string): ConnectionState | undefined; closeAll(): Promise; private trackConnectionClose; }