/** * HarnessEvent: the runner's output vocabulary (PLAN.md Part 0). The * normalizer owns these events and the parsing that produces them; it does * NOT own the chat protocol - lucid maps events into frames downstream. * Only `done` is terminal. A turn may emit MULTIPLE `error` events before * it (a decoded mid-stream error, then a crash-exit stderr tail, etc.), so * a consumer treats `error` as informational and waits for `done`. * * Event classes are load-bearing for backpressure: token and progress * are droppable (coalescible, latest-wins); the rest are lossless. The * split is DECLARED here because the normalizer emits the events; the * coalescing policy that consumes it lives in the chat layer (lucid-v2). * Under token granularity a turn's text arrives twice by design: as token * deltas AND as the whole trailing message event - render one, never * concatenate both. */ import type { CapabilityResult } from "../interpretation/capabilities.js"; import type { ToolDenial, ToolEvent, ToolResultEvent } from "../interpretation/content.js"; import type { QuestionMode } from "../interpretation/question.js"; import type { UsageEvent } from "../interpretation/usage.js"; import type { CompactionState, CompactionTrigger, LimitCode } from "../knowledge/descriptor.js"; import type { NativeApprovalEvent } from "../knowledge/native-approvals.js"; import type { FailureSummary } from "./failure.js"; export type TurnCompletion = "message-delivered" | "native-terminal"; export type ExitCause = | "clean" | "limit" | "crash" | "stall" | "killed" | "failed" /** issue #41: the turn ended by asking (questions ask) - a * SUCCESSFUL turn (process exit 0); the caller resumes with the answer. */ | "awaiting-input"; /** The question mode the turn ran under - the one vocabulary question.ts * owns, recorded on done per RFC-01. */ export type EscalationMode = QuestionMode; export type EscalationDetection = "block" | "malformed" | "none"; export interface EscalationRecord { readonly mode: EscalationMode; readonly detection: EscalationDetection; } export type HarnessEvent = | NativeApprovalEvent | ToolEvent | ToolResultEvent | UsageEvent | { readonly kind: "identity"; readonly sessionId: string; readonly authority: "caller-assigned" | "harness-minted"; readonly capabilities: CapabilityResult; /** RFC-06: present only on a turn that rendered through the * resume-last builder. The announced id was picked by the harness * as most-recent in the spawn cwd, never requested by the caller; * it MAY be a fresh session or a stranger session. On claude the * announced id is the fork id, never the source id. */ readonly resumeLast?: true; } | { readonly kind: "token"; readonly text: string } | { readonly kind: "message"; readonly role: string; readonly text: string } | { readonly kind: "progress"; readonly label: string } /** ADR 0009: the harness compacted its own context. Lossless and ordered - * a dropped compaction event is a boundary the caller cannot rebuild from * anything else on the stream, and a caller that missed it does not know * the history it sent was replaced by a summary. Every numeric field is * the harness's own report; hcn derives none of them, and no harness * pushes an occupancy percentage on a live signal. `detail` is the * harness's own reason or error text: prose, so machine consumers branch * on `state` and never on it (ADR 0002). */ | { readonly kind: "compaction"; readonly state: CompactionState; readonly trigger?: CompactionTrigger; readonly tokensBefore?: number; readonly tokensAfter?: number; readonly durationMs?: number; readonly detail?: string; } | { /** issue #41: the worker asked the caller's user a question (the * final message carried an hcn-question block). Structured-first: * these fields ARE the question; prose renders from them. */ readonly kind: "question"; readonly question: string; readonly options: readonly string[]; readonly recommended?: string; } | { readonly kind: "limit"; readonly code: LimitCode; readonly message: string } /** Advisory only: no failure has been established. Lossless like errors. */ | { readonly kind: "warning"; readonly message: string } /** `terminal: true` marks an error that ended the turn; the matching * `failure` (class task) follows it. `denial` is present only for a * refused tool call or rejected question: the native * tool name and the harness's reason, which may be empty. Machine * consumers branch on its presence, never on prose (ADR 0002). */ | { readonly kind: "error"; readonly message: string; readonly terminal?: boolean; readonly denial?: ToolDenial; } | ({ readonly kind: "failure" } & FailureSummary) | { readonly kind: "done"; readonly exitCode: number | null; readonly cause: ExitCause; /** Positive count of emitted error.denial observations in this turn. * Omitted when none were observed; it does not change the native verdict * or assert that the remaining work succeeded. */ readonly denialCount?: number; /** On a nonzero one-shot exit, output survived the process failure. * message-delivered alone does NOT establish a finished turn; * native-terminal means the harness also reported successful turn end. * Neither asserts that the work itself is correct. */ readonly completion?: TurnCompletion; readonly failure?: FailureSummary; readonly escalation: EscalationRecord; }; export const DROPPABLE_KINDS = new Set(["token", "progress"]);