/** * Headless mode — stdin/stdout JSON protocol for programmatic control. * * Designed for parent processes like the mindstudio-sandbox C&C server. * Input: newline-delimited JSON on stdin (e.g. {"action":"message","requestId":"r1","text":"..."}) * Output: newline-delimited JSON on stdout (e.g. {"event":"text","requestId":"r1","text":"..."}) * * Protocol rules: * - Every stdin command includes an `action` and a caller-provided `requestId`. * - Every stdout event that is a response to a command includes the `requestId`. * - System events (ready, session_restored, stopping, stopped) never have a requestId. * - Every command ends with exactly one `completed` event: * {event:"completed", requestId, success:true|false, error?:string} * - `tool_result` is fire-and-forget (resolves an in-flight promise, no completed event). * * `get_history` is paginated. Request: {action:"get_history", before?:number, * limit?:number, requestId}. `before` is an exclusive upper bound on message * index (defaults to end of array — the most recent messages); `limit` caps * page size (default 500, hard cap 2000). Response: {event:"history", * messages, startIndex, endIndex, totalMessageCount, ...}. Walk backward by * passing the previous response's `startIndex` as the next `before`. When * `startIndex === 0`, no older messages remain. */ interface HeadlessOptions { apiKey?: string; baseUrl?: string; model?: string; lspUrl?: string; } /** * Encapsulates all state and behavior for a headless session. State is * held on instance fields (not closure variables) so mutations are * explicit and greppable. Callbacks passed to external code * (onEvent, onBackgroundComplete, resolveExternalTool, handleStdinLine, * shutdown) are arrow-method fields so `this` is preserved. */ declare class HeadlessSession { private opts; private config; private state; private sessionStats; private running; private currentAbort; /** RequestId of the in-flight message command — injected into streamed events. */ private currentRequestId; /** Guard: track whether terminal `completed` was already sent so we emit exactly one. */ private completedEmitted; private turnStart; /** * Onboarding state of the currently-running turn. Captured at runSingleTurn * start so onBackgroundComplete can enqueue background results with the * right state (the triggering turn's state, not a stale one). */ private currentOnboardingState; /** * Unified message queue. Holds pending work to deliver after the current * turn completes: chained automated actions, background sub-agent results, * and user messages sent while a turn is running. Strict FIFO. Persisted * to .remy-stats.json so queued work survives process restarts. */ private queue; private pendingTools; private earlyResults; private pendingBlockUpdates; private toolRegistry; private stdinBuffer; constructor(opts?: HeadlessOptions); start(): Promise; private shutdown; private emit; /** Emit a `completed` event and mark completedEmitted. Queue state is * surfaced separately via the `queue_changed` event, not on `completed`. */ private emitCompleted; /** Dispatch a simple (non-streaming) command: call handler, emit response + completed. */ private dispatchSimple; /** Persist sessionStats + queue snapshot to .remy-stats.json. */ private persistStats; /** Apply queued tool block updates to state.messages. Safe to call any time. */ private applyPendingBlockUpdates; /** * Forced compaction gate. If lastContextSize exceeds the threshold, compact * before letting the upcoming turn run. Coalesces with any in-flight * compaction (e.g., one already started by /compact or a tool call). No * timeout — compaction takes as long as it takes. * * Lifecycle events (`compaction_started` / `compaction_complete`) and * stats updates are handled by the listener registered in start(); this * method only awaits the promise and applies the resulting summaries. * * On compaction failure we don't bail — the turn proceeds and surfaces any * downstream overflow through the existing "prompt is too long" path. */ private runForcedCompactionIfNeeded; /** Drain pending compaction summaries and insert at a safe point. */ private applyPendingSummaries; private onBackgroundComplete; private resolveExternalTool; private onEvent; /** * Run one turn (without acquiring the `running` lock). Called by * handleMessage for the initial turn, then repeatedly for each queued * message — `running` stays held across the queue drain so no user * message can slip in mid-pipeline. */ private runSingleTurn; private handleMessage; /** * Drain the queue in strict FIFO order. Caller must hold `running = true`. * User messages arriving during the drain will be enqueued behind current items. * * Consecutive background-source items are coalesced into a single turn so * the LLM sees all the background results together and produces one * acknowledgment, not N separate ones. */ private drainQueueLoop; /** * Resume draining the queue when the agent is idle. Acquires the lock, * drains, releases. Used by the `resume` stdin action (sandbox-initiated) * and by kickDrain (background-completion-initiated). */ private resumeQueue; /** * Kick off drainage of the queue when the agent is idle. Used by * onBackgroundComplete (when !running) to deliver results without * racing any currently-synchronous path. */ private kickDrain; private handleClear; /** Change per-agent model picks without clearing history. Takes effect on * the next turn — the model is resolved live, per LLM call, from * `state.models`. Omitting `models` (or sending an empty object) resets * every agent to "use server defaults". */ private handleChangeModels; /** Cancel the running turn and drain the queue. Returns the drained items. */ private handleCancel; /** * Remove pending queued messages — all user messages, or one by id. * Only `source: 'user'` items are removable; chained and background * messages are part of a system chain and are never cancellable. Does * not affect the in-flight turn (use `cancel` for that). */ private handleCancelQueued; private handleStdinLine; } export { type HeadlessOptions, HeadlessSession };