/** * Client side of the fetch carrier. AbstractApiClient holds every protocol invariant: rpcId minting, * four-quadrant envelope wrap/unwrap, zod parsing, in-process SSE frame decoding, and the payload-direct * IApiClient domain methods (business code never mints). Platform differences ride two aspects: * abstract doFetch (transport) + overridable onEnvelope (tap). ApiProxy (the impl face) is untouched. */ import type { z } from 'zod'; import type { ApiProxy, HostFrame, MuxFrame } from '../api/index.ts'; import type { RequestPayload, ResponseValue, RpcMethodMap } from '../api/rpc-map.ts'; import type { ClientResponse, RpcMessage, RpcReceipt, RpcRequest, RpcResponse } from '../api/rpc.ts'; import { RpcId } from '../api/rpc.ts'; /** * Client consumption face of the contract (shape a): same domain tree as ApiProxy, but unary * methods take the business payload directly — the carrier mints the rpcId and wraps the * envelope. Business code needing the call's rpcId reads it from the RpcResponse echo. * Unary methods and respond accept an optional external AbortSignal as the last parameter. * Bounded calls merge it with the instance timeout via AbortSignal.any; user-paced calls * carry only that external signal. In both cases the signal rides beside the request, never * on the wire, like the stream signatures. * Stream methods accept an optional onOpen callback: it fires once the physical transport is * readable (before any frame) — the "stream established" signal * connection controllers need for the readiness handshake. Generators are lazy, so the * underlying fetch (and therefore onOpen) only happens once iteration starts. * Relationship: ApiProxy is the narrow-form signature contract the impl side implements; * IApiClient is the payload-direct view clients consume; AbstractApiClient bridges the two. * Derived per method key from RpcMethodMap so a map row addition updates this mechanically. */ export interface IApiClient { sessions: { list(payload: RequestPayload<'session.list'>, signal?: AbortSignal): Promise>>; search(payload: RequestPayload<'session.search'>, signal?: AbortSignal): Promise>>; create(payload: RequestPayload<'session.create'>, signal?: AbortSignal): Promise>>; history(payload: RequestPayload<'session.history'>, signal?: AbortSignal): Promise>>; models(payload: RequestPayload<'session.models'>, signal?: AbortSignal): Promise>>; selectModel(payload: RequestPayload<'session.selectModel'>, signal?: AbortSignal): Promise>>; rename(payload: RequestPayload<'session.rename'>, signal?: AbortSignal): Promise>>; fork(payload: RequestPayload<'session.fork'>, signal?: AbortSignal): Promise>>; prompt(payload: RequestPayload<'session.prompt'>, signal?: AbortSignal): Promise>>; attachment(payload: RequestPayload<'session.attachment'>, signal?: AbortSignal): Promise>>; updateQueue(payload: RequestPayload<'session.updateQueue'>, signal?: AbortSignal): Promise>>; cancel(payload: RequestPayload<'session.cancel'>, signal?: AbortSignal): Promise>>; }; subagents: { list(payload: RequestPayload<'subagent.list'>, signal?: AbortSignal): Promise>>; history(payload: RequestPayload<'subagent.history'>, signal?: AbortSignal): Promise>>; prompt(payload: RequestPayload<'subagent.prompt'>, signal?: AbortSignal): Promise>>; interrupt(payload: RequestPayload<'subagent.interrupt'>, signal?: AbortSignal): Promise>>; }; host: { describe(payload: RequestPayload<'host.describe'>, signal?: AbortSignal): Promise>>; pickDirectory(payload: RequestPayload<'host.pickDirectory'>, signal?: AbortSignal): Promise>>; listDirectory(payload: RequestPayload<'host.listDirectory'>, signal?: AbortSignal): Promise>>; createDirectory(payload: RequestPayload<'host.createDirectory'>, signal?: AbortSignal): Promise>>; openPath(payload: RequestPayload<'host.openPath'>, signal?: AbortSignal): Promise>>; }; workspace: { list(payload: RequestPayload<'workspace.list'>, signal?: AbortSignal): Promise>>; create(payload: RequestPayload<'workspace.create'>, signal?: AbortSignal): Promise>>; rename(payload: RequestPayload<'workspace.rename'>, signal?: AbortSignal): Promise>>; delete(payload: RequestPayload<'workspace.delete'>, signal?: AbortSignal): Promise>>; insertSessionBefore(payload: RequestPayload<'workspace.insertSessionBefore'>, signal?: AbortSignal): Promise>>; archiveSession(payload: RequestPayload<'workspace.archiveSession'>, signal?: AbortSignal): Promise>>; }; commands: { list(payload: RequestPayload<'command.list'>, signal?: AbortSignal): Promise>>; execute(payload: RequestPayload<'command.execute'>, signal?: AbortSignal): Promise>>; }; skills: { list(payload: RequestPayload<'skill.list'>, signal?: AbortSignal): Promise>>; }; agentPresets: { list(payload: RequestPayload<'agentPreset.list'>, signal?: AbortSignal): Promise>>; select(payload: RequestPayload<'agentPreset.select'>, signal?: AbortSignal): Promise>>; read(payload: RequestPayload<'agentPreset.read'>, signal?: AbortSignal): Promise>>; copy(payload: RequestPayload<'agentPreset.copy'>, signal?: AbortSignal): Promise>>; openDocument(payload: RequestPayload<'agentPreset.openDocument'>, signal?: AbortSignal): Promise>>; remove(payload: RequestPayload<'agentPreset.remove'>, signal?: AbortSignal): Promise>>; }; events: { mux(payload: Parameters[0]['payload'], signal: AbortSignal, onOpen?: () => void): AsyncIterable>; host(payload: Parameters[0]['payload'], signal: AbortSignal, onOpen?: () => void): AsyncIterable>; }; goals: { create(payload: RequestPayload<'goal.create'>, signal?: AbortSignal): Promise>>; edit(payload: RequestPayload<'goal.edit'>, signal?: AbortSignal): Promise>>; pause(payload: RequestPayload<'goal.pause'>, signal?: AbortSignal): Promise>>; resume(payload: RequestPayload<'goal.resume'>, signal?: AbortSignal): Promise>>; complete(payload: RequestPayload<'goal.complete'>, signal?: AbortSignal): Promise>>; clear(payload: RequestPayload<'goal.clear'>, signal?: AbortSignal): Promise>>; }; settings: { describe(payload: RequestPayload<'settings.describe'>, signal?: AbortSignal): Promise>>; openDocument(payload: RequestPayload<'settings.openDocument'>, signal?: AbortSignal): Promise>>; update(payload: RequestPayload<'settings.update'>, signal?: AbortSignal): Promise>>; replace(payload: RequestPayload<'settings.replace'>, signal?: AbortSignal): Promise>>; mutate(payload: RequestPayload<'settings.mutate'>, signal?: AbortSignal): Promise>>; }; credentials: { describe(payload: RequestPayload<'credentials.describe'>, signal?: AbortSignal): Promise>>; set(payload: RequestPayload<'credentials.set'>, signal?: AbortSignal): Promise>>; unset(payload: RequestPayload<'credentials.unset'>, signal?: AbortSignal): Promise>>; }; llm: { providers(payload: RequestPayload<'llm.providers'>, signal?: AbortSignal): Promise>>; models(payload: RequestPayload<'llm.models'>, signal?: AbortSignal): Promise>>; discoverModels(payload: RequestPayload<'llm.discoverModels'>, signal?: AbortSignal): Promise>>; }; /** client-response passthrough (rpcId is a backfill of the server-request's id — never minted here). */ respond(message: ClientResponse, signal?: AbortSignal): Promise; } /** Whether a unary call uses the transport health deadline or only caller/connection cancellation. */ type UnaryTimeoutPolicy = 'default' | 'caller-signal-only'; /** * Abstract fetch-carrier client. Subclasses supply the transport (doFetch) and may refine the * per-message tap (onEnvelope) — platform aspects stay in subclasses, protocol invariants stay * here. Envelope observation is a first-class aspect of this data middle layer: the instance * owns a microtask-batched buffer (frame storms must not cost one consumer update per frame), * and observers subscribe via subscribeEnvelopes. The isomorphic point survives: an in-process * subclass whose doFetch is toFetchHandler(api).fetch never touches the network. */ export declare abstract class AbstractApiClient implements IApiClient { protected readonly timeoutMs: number; /** Instance-owned observation buffer (module-level state would leak across instances/tests). */ private envelopeBatch; private flushScheduled; private readonly envelopeListeners; /** @param timeoutMs - timeout for bounded unary calls; user-paced calls and streams do not use it. */ constructor(timeoutMs?: number); /** Transport aspect: browser fetch, injected handler.fetch, IPC bridge, ... */ protected abstract doFetch(input: URL, init?: RequestInit): Promise; /** * Subscribe to batched envelope observation (diagnostics/logging consumers). * Batches follow microtask boundaries; a listener throw is isolated (observation * must never break the carrier). * @param listener - receives each flushed batch in arrival order. * @returns unsubscribe function. */ subscribeEnvelopes(listener: (batch: readonly RpcMessage[]) => void): () => void; /** Per-message tap: feeds the instance buffer. Subclasses may override to observe unbatched (call super to keep batching). */ protected onEnvelope(message: RpcMessage): void; /** Browser = same-origin (a fake authority would fail DNS on real requests); no-location env (Node) = fake authority. */ protected resolveBase(): string; protected mintRpcId(): RpcId; /** * Shared POST leg of both C→S carriers (callUnary/respond): JSON body, * optional default timeout merged with the caller's external signal, non-2xx → transport throw. */ private postJson; /** * Unary protocol path: mint → tap → POST full form → envelope parse → verify * echo → value parse → tap → narrow. Virtual so a fake carrier (fixture) can * override transport at this layer. */ protected callUnary(method: K, payload: RequestPayload, signal?: AbortSignal, timeoutPolicy?: UnaryTimeoutPolicy): Promise>>; /** Mux stream opener; virtual for the same override reason as callUnary. */ protected openMux(_payload: Parameters[0]['payload'], signal: AbortSignal, onOpen?: () => void): AsyncIterable>; /** Host stream opener; virtual. */ protected openHost(_payload: Parameters[0]['payload'], signal: AbortSignal, onOpen?: () => void): AsyncIterable>; /** * SSE protocol path: streaming fetch (not EventSource), '\n\n' framing, ServerRequest envelope + * frame-schema parse, tap, narrow yield. onOpen fires once the response headers are in and the * body is readable — the stream-established signal, before any frame arrives. A frame that fails * either parse level is reported and skipped (one corrupt frame must not kill the stream; the * client's gap detection covers whatever the frame carried). */ protected readSse(path: string, signal: AbortSignal, frameSchema: z.ZodType, onOpen?: () => void): AsyncGenerator>; readonly sessions: IApiClient['sessions']; readonly subagents: IApiClient['subagents']; readonly host: IApiClient['host']; readonly workspace: IApiClient['workspace']; readonly commands: IApiClient['commands']; readonly skills: IApiClient['skills']; readonly agentPresets: IApiClient['agentPresets']; readonly goals: IApiClient['goals']; readonly settings: IApiClient['settings']; readonly credentials: IApiClient['credentials']; readonly llm: IApiClient['llm']; readonly events: IApiClient['events']; respond(message: ClientResponse, signal?: AbortSignal): Promise; } /** * In-process client over an injected fetch-shaped handler (the isomorphic point: * `new InProcessApiClient(toFetchHandler(api))` never touches the network). Lives here because * in-process injection is this package's own capability (handler and client are both local). */ export declare class InProcessApiClient extends AbstractApiClient { private readonly handler; constructor(handler: { fetch: typeof fetch; }, timeoutMs?: number); /** * Faithful to real fetch: reject on signal abort even when the in-process * handler ignores the signal (a hung impl must not defeat timeout/cancel). */ protected doFetch(input: URL, init?: RequestInit): Promise; } export {}; //# sourceMappingURL=client.d.ts.map