/** * HarnessDriver — abstract base contract every harness implementation honors. * * Pattern adapted from t3code's `ProviderDriver`. Each driver wraps one CLI * (codex, claude-code, opencode, hermes, openclaw, ollama, gemini) and emits * canonical `ProviderRuntimeEvent`s into an Effect Stream. * * The base class owns: * - the SessionMachine and per-turn TurnMachine * - monotonic event sequencing * - event emission helpers that stamp envelope fields automatically * - typed error mapping * * Subclasses implement transport: subprocess management, stdout parsing, * stdin marshalling, cancellation. Subclasses do not handle event envelope * boilerplate. */ import { Effect, Stream } from "effect"; import { type HarnessId, type ProviderInstanceId, type SessionId, type ThreadId, type ToolCallId, type TurnId } from "../contracts/ids.js"; import { type HarnessError } from "../contracts/errors.js"; import type { ProviderRuntimeEvent, ProviderRuntimeEventBase, SessionState } from "../contracts/provider-runtime.js"; import { SessionMachine } from "../state/session-machine.js"; import { TurnMachine } from "../state/turn-machine.js"; import { type HarnessSnapshot, type SnapshotStore } from "../snapshots/snapshot-store.js"; import type { EventJournal } from "../journal/event-journal.js"; import type { EventBus } from "../event-bus.js"; export interface HarnessDriverConfig { readonly harnessId: HarnessId; readonly providerInstanceId: ProviderInstanceId; readonly sessionId: SessionId; readonly threadId?: ThreadId; /** Implementation-specific knobs (model, effort, base URL, env). */ readonly options?: Record; /** * Optional snapshot store. When provided, the driver writes a snapshot * after every state-changing event so a runtime restart can resume the * session. When absent, the driver behaves exactly as before. */ readonly snapshotStore?: SnapshotStore; /** * Optional event journal. When provided, every emitted event is appended * to a per-session JSONL log so a UI client can replay the session from * a cursor (offline playback, audit, post-hoc rendering). When absent, * the driver behaves exactly as before. */ readonly eventJournal?: EventJournal; /** * Optional shared event bus. When provided, every emitted event is also * published to the bus so cross-cutting primitives (checkpoint-store, * hook-runner, walkthrough-writer, etc.) can subscribe instead of being * called explicitly from the tool broker. */ readonly eventBus?: EventBus; } export interface HarnessTurnRequest { readonly turnId: TurnId; readonly prompt: string; readonly attachments?: ReadonlyArray<{ kind: string; ref: string; }>; } export interface HarnessDriver { readonly config: HarnessDriverConfig; start(): Effect.Effect; sendTurn(req: HarnessTurnRequest): Effect.Effect; interrupt(reason?: string): Effect.Effect; shutdown(): Effect.Effect; events(): Stream.Stream; sessionState(): SessionState; } export declare abstract class BaseHarnessDriver implements HarnessDriver { readonly config: HarnessDriverConfig; protected readonly session: SessionMachine; protected currentTurn: TurnMachine | null; protected currentTurnId: TurnId | null; private seq; private readonly subscribers; private finalized; constructor(config: HarnessDriverConfig); abstract start(): Effect.Effect; abstract sendTurn(req: HarnessTurnRequest): Effect.Effect; abstract interrupt(reason?: string): Effect.Effect; abstract shutdown(): Effect.Effect; sessionState(): SessionState; events(): Stream.Stream; protected nextSeq(): number; protected envelope(extra?: { turnId?: TurnId; }): ProviderRuntimeEventBase; protected emit(event: ProviderRuntimeEvent): void; private maybePublishToBus; private journalWriteInflight; private maybeAppendJournal; private totalInputTokens; private totalOutputTokens; private totalReasoningTokens; private estimatedCostUsd; private lastError; private snapshotCreatedAt; private snapshotWriteInflight; /** * Returns the cumulative usage + last-error stats accumulated from emitted * events. Useful for tests and the snapshot writer. */ protected snapshotStats(): { totalInputTokens: number; totalOutputTokens: number; totalReasoningTokens: number; estimatedCostUsd: number; lastError: string | undefined; createdAt: string; }; /** * Restore mutable driver state from a previously-saved snapshot. Called * by subclasses inside start() when resuming after a runtime restart. * * Only resumes non-terminal sessions/turns. If the snapshot says the * session is stopped/error or the turn is done/failed, those are left as * fresh-start. */ protected restoreFromSnapshot(snap: HarnessSnapshot): void; /** * Returns the persisted snapshot for this session if one exists. Subclasses * call this inside start() to decide whether to resume or start fresh. */ protected loadSnapshot(): Promise; private maybeUpdateSnapshot; protected emitToolStarted(toolCallId: ToolCallId, tool: string, args: unknown): void; protected emitToolOutput(toolCallId: ToolCallId, chunk: string, stream?: "stdout" | "stderr"): void; protected emitToolCompleted(toolCallId: ToolCallId, ok: boolean, elapsedMs: number, extras?: { result?: unknown; error?: string; }): void; protected emitMessageDelta(delta: string): void; protected emitMessageComplete(text: string): void; protected emitUserMessage(text: string): void; protected emitDiagnostic(level: "warn" | "error", message: string, data?: unknown): void; protected emitSessionStateChange(toState: SessionState, reason?: string): void; protected emitTurnStateChange(fromState: "pending" | "running" | "done" | "failed", toState: "pending" | "running" | "done" | "failed", turnId?: TurnId): void; protected wrapError(unknown: unknown, turnId?: TurnId): HarnessError; protected finalize(): void; }