///|
/// One event on the engine's stdout JSONL stream — the wire contract between
/// `openseek run`/`openseek serve` and every client that reads them (the TUI,
/// the desktop host, and any script consuming `run`'s stdout).
///
/// The stream is a protocol, not a log. `emit` (in `openseek_protocol/emit`)
/// writes each event's line — exactly its flat `to_json` fields — straight to
/// stdout through its own writer, never through a logger — the CLI links none
/// — so a client reading stdout sees only events.
///
/// There is no severity on the line: a client that wants one derives it from
/// the variant it decoded. `emit` is the only writer; `parse` is its inverse.
pub(all) enum Event {
  // ── agent loop ─────────────────────────────────────────────────────────
  AgentSetupFailed(error~ : String)
  AgentStep(step~ : Int)
  /// The current step's provider request failed and attempt `attempt` (of
  /// `max_attempts`) is about to be made, after the client's backoff. It is
  /// emitted when the retry is decided, so a controller can discard the failed
  /// attempt's live output and show the retry while the client waits. This is
  /// transient controller state: it is emitted on the live JSONL stream, never
  /// represented by `agent_session.SessionItem`, and therefore never appended
  /// to `session.jsonl`.
  StreamRetry(attempt~ : Int, max_attempts~ : Int, reason~ : String)
  AgentAborted(reason~ : String)
  AgentFinished(answer~ : String)
  MaxStepsExhausted
  TurnFailed(error~ : String)
  AssistantDelta(content~ : String)
  /// One transient provider reasoning fragment. Consumers may render or ignore
  /// these high-volume progress events; completed reasoning still emits once
  /// as `ReasoningMessage` and is stored with its assistant response.
  ReasoningDelta(content~ : String)
  AssistantMessage(content~ : String)
  ReasoningMessage(content~ : String)
  Usage(usage~ : Usage)
  /// `brief` is always written, `null` when the tool reported none: an absent
  /// key and a null one are indistinguishable to every decoder, so the shape
  /// stays uniform across the three call sites that emit a tool result.
  ToolResult(
    tool_call_id~ : String,
    tool_name~ : String,
    is_error~ : Bool,
    content~ : String,
    brief~ : String?
  )
  ToolCallDecodeError(
    tool_call_id~ : String,
    tool_name~ : String,
    error~ : String
  )

  // ── approval ───────────────────────────────────────────────────────────
  /// A tool is asking the controller for permission and is BLOCKED until an
  /// `approval` command carrying this `id` comes back. The one event on this
  /// stream that is a question rather than a report, and the only one a
  /// controller MUST answer: there is no deadline behind it, so a controller
  /// that reads events and never writes one leaves the turn stopped until
  /// somebody cancels it.
  ///
  /// `id` is minted by the engine and is unique within one engine process.
  /// `body` is what would actually RUN, verbatim — the `mbtx` program —
  /// while `detail` describes only what would be granted. A controller that
  /// renders one without the other is asking someone to approve a permission
  /// without showing them what it is for.
  ApprovalRequested(
    id~ : String,
    tool_name~ : String,
    detail~ : String,
    body~ : String?
  )
  /// How a previously requested approval settled — including when it settled
  /// without the controller's answer — the turn was interrupted, or a second
  /// controller answered first. Purely informational: a controller uses it to
  /// retire the prompt it is showing.
  ///
  /// `outcome` is `"allowed_once"`, `"rejected"`, or `"cancelled"`. The
  /// runtime's fourth outcome, `unavailable`, never appears here: it is what an
  /// engine with no controller answers, and such an engine emits no request in
  /// the first place.
  ApprovalResolved(id~ : String, outcome~ : String)

  // ── steering ───────────────────────────────────────────────────────────
  SteerApplied(kind~ : String, content~ : String)
  SteerDropped(content~ : String)
  BackgroundNotice(content~ : String)

  // Background execution events are session-scoped and may arrive between turns.
  JobChanged(kind~ : JobEventKind, job~ : JobRecord)
  JobsSnapshot(request_id~ : String, jobs~ : Array[JobRecord])
  JobStopResult(
    request_id~ : String,
    generation~ : String,
    job_id~ : String,
    outcome~ : JobStopOutcome
  )

  // ── goal ───────────────────────────────────────────────────────────────
  GoalUpdated(goal~ : String?)
  GoalBlocked(reason~ : String)
  GoalUnblocked
  GoalCheck(content~ : String)
  GoalReminder(content~ : String)
  GoalContinue(remaining~ : Int)
  GoalBudgetExhausted(turns~ : Int)

  // ── plan ───────────────────────────────────────────────────────────────
  PlanReminder(content~ : String)

  // ── compaction ─────────────────────────────────────────────────────────
  CompactionStarted(from_sequence~ : Int, to_sequence~ : Int)
  CompactionFinished(
    from_sequence~ : Int,
    to_sequence~ : Int,
    summary~ : String
  )
  /// Also reported when the cause was cancellation; how loudly to show it is
  /// the client's call, made from the variant.
  CompactionFailed(error~ : String)
  AutoCompactionStarted(from_sequence~ : Int, to_sequence~ : Int)
  AutoCompactionFinished(
    from_sequence~ : Int,
    to_sequence~ : Int,
    summary~ : String
  )
  AutoCompactionFailed(error~ : String)
  ContextYield(to_sequence~ : Int, answer~ : String)

  // ── subrun ─────────────────────────────────────────────────────────────
  /// A nested sub-run (a review, an explore) began inside this run. `id` is
  /// unique within the stream so overlapping sub-runs pair each start with
  /// its finish; `kind` names the surface ("review", "explore"); `label` is
  /// a short, DISPLAY-BOUNDED string (typically a truncated, sanitized
  /// query) — never unbounded raw input.
  SubrunStarted(id~ : String, kind~ : String, label~ : String)
  /// The paired completion. The emitter's contract (the sub-run runner —
  /// wire-first: this variant ships ahead of it): emit on success, failure,
  /// AND cancellation, and BEFORE the parent turn's own terminal event (the
  /// TUI's stale-run guard drops run-tagged events after a terminal), so a
  /// reader never shows a sub-run as running forever. `status` is the
  /// sub-run terminal in snake_case ("captured", "no_report", "max_steps",
  /// "context_yield", "timed_out", "failed", "cancelled"); `steps` and the
  /// token counts are the child's actual spend, for cost attribution.
  SubrunFinished(
    id~ : String,
    status~ : String,
    steps~ : Int,
    prompt_tokens~ : Int,
    completion_tokens~ : Int
  )

  // ── workflow ───────────────────────────────────────────────────────────
  /// A `mbtx` snippet started a workflow: a run that delegates to child
  /// agents of its own, through `moonbitlang/workflow`.
  ///
  /// The engine emits this because nothing else can. The workflow library is
  /// generic — it never learns openseek exists — so a run inside a snippet
  /// would otherwise be invisible: the parent sees one opaque `mbtx` tool
  /// call, however many children it starts. What mbtx knows, and says here,
  /// is where it told the run to write and which child ordinals it reserved
  /// for it. Discovery is therefore not discovery; a reader is told where to
  /// look, exactly as `SubrunStarted` tells it a sub-run began.
  ///
  /// `journal` is an append-only JSONL ledger of RESOLVED calls (phase,
  /// timings, spend); `events` is the sidecar that reports a launch when it
  /// STARTS, which the journal cannot — an entry carries an outcome, so it
  /// lands only at completion. Both are absolute paths a reader tails.
  ///
  /// `first_child`/`child_count` bound the reserved ordinals: the child
  /// sessions of this run are exactly `-sr-N` for N in
  /// `[first_child, first_child + child_count)`, which is how a reader knows
  /// which transcripts belong to it.
  WorkflowStarted(
    id~ : String,
    journal~ : String,
    events~ : String,
    first_child~ : Int,
    child_count~ : Int
  )
  /// The paired completion, emitted on every exit of the snippet that owns
  /// the run — success, failure, and cancellation — so a reader never shows
  /// a workflow as running forever.
  WorkflowFinished(id~ : String)

  // ── session / workspace ────────────────────────────────────────────────
  /// `workspace_root` is optional because it postdates the event: engines
  /// between d11e04c2 (which added `session_started`) and d5d0b208 (which
  /// added `--dir`) emit only `session` and `session_root`. No client reads it.
  SessionStarted(
    session~ : String,
    session_root~ : String,
    workspace_root~ : String?
  )
  SessionError(error~ : String)
  WorkspaceCreated(dir~ : String)
  CommandError(error~ : String)
  FleetStarted(runs~ : Int, task~ : String)

  // ── mcp ────────────────────────────────────────────────────────────────
  McpConfigIgnored(reason~ : String)
  McpConfigUnreadable(path~ : String, error~ : String)
  McpConfigInvalid(path~ : String, error~ : String)
  McpToolsRegistered(servers~ : Int, tools~ : Int, names~ : Array[String])
  McpToolDuplicate(server~ : String, tool~ : String)
  McpToolRenamed(from~ : String, to~ : String)
  McpToolsCapped(server~ : String, kept~ : Int)
  McpServerSkippedOverCap(server~ : String)
  /// `error` is the connect failure's text: the spawn or transport error, a
  /// handshake error, or the handshake timeout.
  McpConnectFailed(server~ : String, error~ : String)
  McpListToolsFailed(server~ : String, error~ : String)
  McpListToolsTimeout(server~ : String)
  McpNoTools(server~ : String)
} derive(Eq, Debug)

///|
pub extend Event with Debug::{to_repr}

///|
pub extend Event with Eq::{equal, not_equal}