/** * `agent-relay passthrough ` — read-write attach in passthrough session. * * The broker auto-injects inbound relay messages into the agent's PTY * while the human also types; both writers race. That's the point — * passthrough is for observe-and-occasionally-nudge sessions * while the broker does its coordination thing. For exclusive * deterministic control with no auto-inject, use `drive` instead. * * On attach, ensures the worker is in `auto_inject` delivery mode (it's the * broker default, but if someone left a `drive` session the worker may * be in `manual_flush` mode — `passthrough` flips it back for the session's * duration and restores the prior mode on detach). On detach, restores * the prior mode and leaves the agent running. * * The session loop (snapshot-on-attach, raw stdin, resize forwarding, * Ctrl+C detach) mirrors the shape of * `drive.ts` minus the pending-queue UI and manual delivery controls * (there's no queue in passthrough session). `drive.ts` is the more * heavily-commented version of the shared shape; this module * duplicates rather than abstracts because the trimmed surface is * small enough that an extra layer of indirection would cost more * clarity than it saves. */ import { Buffer } from 'node:buffer'; import WebSocket from 'ws'; import { captureAndRenderSnapshot, type AttachSnapshotConnection, type AttachSnapshotDeps } from '../lib/attach.js'; import { type BrokerConnection } from '../lib/broker-connection.js'; import { type CliPtyInputStream, type InboundDeliveryMode } from './attach-drive.js'; import { type CreatePredictiveEchoOptions } from './predictive-echo-screen.js'; import type { PredictiveEcho } from '@agent-relay/harness-driver'; type ExitFn = (code: number) => never; /** Minimal WebSocket surface we depend on — same shape as `drive`'s. */ export interface PassthroughWebSocket { on(event: 'open', listener: () => void): unknown; on(event: 'message', listener: (data: WebSocket.RawData) => void): unknown; on(event: 'close', listener: (code: number, reason: Buffer) => void): unknown; on(event: 'error', listener: (err: Error) => void): unknown; close(code?: number, reason?: string): void; } export type PassthroughWebSocketFactory = (url: string, headers: Record) => PassthroughWebSocket; export interface PassthroughSignalRegistrar { (signal: NodeJS.Signals, handler: () => void | Promise): void | (() => void); } export interface PassthroughStdin { setRawMode?: (mode: boolean) => unknown; isTTY?: boolean; resume(): unknown; pause(): unknown; on(event: 'data', listener: (chunk: Buffer) => void): unknown; off?(event: 'data', listener: (chunk: Buffer) => void): unknown; removeListener?(event: 'data', listener: (chunk: Buffer) => void): unknown; } export interface PassthroughTerminal { getSize(): { rows: number; cols: number; } | null; onResize(handler: () => void): () => void; } export interface PassthroughDependencies { readConnectionFile: (stateDir: string) => unknown; getDefaultStateDir: () => string; env: NodeJS.ProcessEnv; createWebSocket: PassthroughWebSocketFactory; writeChunk: (chunk: string) => void; onSignal: PassthroughSignalRegistrar; log: (...args: unknown[]) => void; error: (...args: unknown[]) => void; exit: ExitFn; fetch: typeof globalThis.fetch; captureAndRenderSnapshot: (connection: AttachSnapshotConnection, name: string, deps: AttachSnapshotDeps) => ReturnType; stdin: PassthroughStdin; terminal: PassthroughTerminal; /** Opens the SDK PTY input stream used for raw human keystrokes. */ openInputStream: (connection: BrokerConnection, name: string) => CliPtyInputStream; /** * Builds the adaptive predictive-echo engine, or returns null to disable * it (degenerate terminal). Omitted by tests that want plain pass-through. */ createPredictiveEcho?: (opts: CreatePredictiveEchoOptions) => PredictiveEcho | null; } /** Discriminated union of broker events the `passthrough` client cares * about. No `delivery_queued` / `agent_pending_drained` — there's no * queue in passthrough session, so those events (which the broker doesn't * emit while the worker is in `auto_inject`) would be `other`. */ export type PassthroughWsEvent = { kind: 'worker_stream'; chunk: string; } | { kind: 'other'; }; /** * Inspect a single WebSocket frame and classify it relative to the * agent we're following. Non-matching / malformed frames return * `{ kind: 'other' }` so the caller can ignore them cheaply. */ export declare function classifyWsEvent(rawMessage: string, name: string): PassthroughWsEvent; /** ----- Keybind state machine ----- */ export interface PassthroughKeybindOutcome { forward: Buffer; actions: PassthroughKeybindAction[]; } export type PassthroughKeybindAction = 'detach'; /** * Parser for the one local control byte passthrough keeps: `Ctrl+C` detaches. * * Semantics: * - `Ctrl+C` (0x03) → emit `detach`, never forwarded. * - Every other byte, including Ctrl+B and Ctrl+G, is forwarded to the agent. */ export declare class PassthroughKeybindParser { feed(chunk: Buffer): PassthroughKeybindOutcome; reset(): void; } /** ----- Status line rendering ----- */ /** * Render the bottom-of-terminal status line for `passthrough`. Same * save/restore-cursor trick as `drive`, no pending counter (there * isn't one in passthrough session). */ export declare function renderStatusLine(opts: { name: string; mode: InboundDeliveryMode; rows?: number; }): string; /** ----- Main session runner ----- */ /** * Open a `passthrough` session. Resolves with the exit code the CLI * should propagate. Cleans up its own stdin raw-mode and best-effort * restores the worker's previous inbound delivery mode on any exit path. */ export declare function runPassthroughSession(agentName: string, options: { brokerUrl?: string; apiKey?: string; stateDir?: string; }, deps: PassthroughDependencies): Promise; /** Run a passthrough session with default dependencies. Used by `runtime agent attach --mode passthrough`. */ export declare function attachPassthrough(name: string, options: { brokerUrl?: string; apiKey?: string; stateDir?: string; }, overrides?: Partial): Promise; export {}; //# sourceMappingURL=attach-passthrough.d.ts.map