///|
/// One request to run a sub-agent: the workflow-side view of a call before
/// any engine specifics (binary path, model, endpoint) attach to it.
/// `kind` names the child mode the engine dispatches on (`explore`,
/// `echo`, later `worker`); `input` is the EXACT JSON the child receives
/// as its input line — the persisted replay identity IS the child input,
/// so an encoder change is an identity change and can never replay stale
/// work. `label` is display metadata: journal replay matches on (kind,
/// input, max_steps, schema, scope), never on the label. By convention a
/// label reads `stage:instance` (`verify:claude:3`, `survey:journal`), so
/// a reader can group the instances of one stage by the prefix before the
/// first colon.
///
/// The workflow BUILDS one and engines READ it; a caller seeding a journal
/// by hand goes through `AgentCall(...)`, so the replay identity can grow
/// a field without invalidating every hand-written call.
pub struct AgentCall {
  kind : String
  input : Json
  label : String
  max_steps : Int?
  /// Replay namespace: everything OUTSIDE the input that makes two
  /// otherwise-identical calls non-interchangeable — model, prompt
  /// revision, tenant, workspace. Empty means "unscoped".
  scope : String
  /// A JSON Schema the report must satisfy, when the caller asked for one.
  /// It rides the request so an engine that can constrain the
  /// model does, and it is part of the work identity: the same question
  /// under a different shape is different work. Absent from the journal
  /// line when `None`, so pre-schema journals load unchanged.
  schema : Json?
} derive(Eq, ToJson, FromJson)

///|
/// A call from its parts: the work (`kind`, `input`) and how a reader
/// names it (`label`). The rest is optional because the common call has
/// no ceiling, no shape, and no namespace — `scope` defaults to unscoped,
/// which is what `""` means on the field.
pub fn AgentCall::AgentCall(
  kind~ : String,
  input~ : Json,
  label~ : String,
  max_steps? : Int,
  scope? : String = "",
  schema? : Json,
) -> AgentCall {
  { kind, input, label, max_steps, scope, schema, }
}

///|
/// The cost accounting of one attempt to run an agent, captured whether or
/// not the attempt produced a report: a timed-out child spent real tokens,
/// and losing that spend would understate every budget built on top.
///
/// Engines BUILD one through `AgentAttempt(...)`; everyone else READS the
/// fields. That split is what lets a later accounting figure arrive as one
/// more optional argument instead of breaking every engine that ever
/// wrote the record out by hand.
pub struct AgentAttempt {
  attempt_id : String
  steps_used : Int
  prompt_tokens : Int
  completion_tokens : Int
  /// The engine's own money figure for the attempt, when it reports one
  /// (`usage.cost_usd` on the wire, summed). `None` means unknown, never
  /// free: an engine that prices nothing leaves it absent. Absent from the
  /// journal line when `None`, so earlier journals load unchanged.
  cost_usd : Double?
  /// Why the counters above may fall short of what the attempt spent:
  /// `Some(reason)` when the engine could not give its own account (a child
  /// that died without a result, say), so the counters are only what was
  /// observed. `None` means they are the engine's account. Absent from the
  /// journal line when `None`, like `cost_usd`.
  unaccounted : String?
} derive(Eq, ToJson, FromJson)

///|
/// An attempt's accounting from its parts. Everything an engine always
/// knows is named; `cost_usd` is optional because an engine that prices
/// nothing simply leaves it out — which is what `None` means on the field:
/// unknown, never free.
pub fn AgentAttempt::AgentAttempt(
  attempt_id~ : String,
  steps_used~ : Int,
  prompt_tokens~ : Int,
  completion_tokens~ : Int,
  cost_usd? : Double,
  unaccounted? : String,
) -> AgentAttempt {
  {
    attempt_id,
    steps_used,
    prompt_tokens,
    completion_tokens,
    cost_usd,
    unaccounted,
  }
}

///|
/// Why an agent produced no usable report — the workflow-facing vocabulary
/// a script can meaningfully react to. Transient transport errors never
/// appear here: retrying those is the engine's job, below this seam. What
/// a script retries is a whole AGENT that came back with nothing usable —
/// `retry`, whose default policy is written against this vocabulary.
/// The child contract's result statuses other than `completed` map onto
/// these (`docs/child-contract.md` §4), plus `Skipped` (a human declined
/// the call — replay keeps it declined rather than overriding the
/// decision).
pub(all) enum AgentFailure {
  TimedOut
  NoReport
  MaxSteps
  ContextYield
  Skipped
  Failed(String)
} derive(Eq, ToJson, FromJson)

///|
/// The lossless envelope one agent run resolves to: either a report value
/// with its attempt accounting, or a failure that STILL carries the
/// attempt's cost. `attempt=None` means no launch was ever TRIED — a
/// `Skipped` call, or a refusal before spawn; a child that failed to
/// spawn or died early is an attempt that observed zero cost, not an
/// absent one. This is the unit the journal persists: replaying it must
/// lose nothing the live run knew.
pub(all) enum AgentOutcome {
  Finished(value~ : Json, attempt~ : AgentAttempt)
  DidNotFinish(failure~ : AgentFailure, attempt~ : AgentAttempt?)
} derive(Eq, ToJson, FromJson)

///|
/// The attempt this outcome accounts for: always there for `Finished`, and
/// for `DidNotFinish` unless no launch was ever tried.
pub fn AgentOutcome::attempt(self : AgentOutcome) -> AgentAttempt? {
  match self {
    Finished(attempt~, ..) => Some(attempt)
    DidNotFinish(attempt~, ..) => attempt
  }
}

///|
/// The typed error channel of the workflow layer. Every call site either
/// propagates one of these (via `raise`) or catches it into `attempt`'s
/// `Result` — there is no silent `null` to forget.
pub suberror WorkflowError {
  /// The named agent call produced no usable report, and why.
  AgentFailed(label~ : String, failure~ : AgentFailure)
  /// The workflow's launch allowance was already spent when this call was
  /// about to launch — the runaway backstop. Only LAUNCHED agents consume
  /// allowance; a call cancelled while queued never counts.
  CallBudgetExhausted(label~ : String)
  /// A quorum/collect policy over a fan-out did not reach its threshold:
  /// `ok` of `of` succeeded, `need` were required.
  QuorumNotReached(need~ : Int, ok~ : Int, of~ : Int)
}

///|
pub impl Show for AgentFailure with fn output(self, logger) {
  let text = match self {
    TimedOut => "timed out"
    NoReport => "finished without a report"
    MaxSteps => "exhausted its step ceiling"
    ContextYield => "yielded at the context ceiling"
    Skipped => "was skipped by the user"
    Failed(reason) => "failed: \{reason}"
  }
  logger.write_string(text)
}

///|
pub impl Show for WorkflowError with fn output(self, logger) {
  let text = match self {
    AgentFailed(label~, failure~) => "agent '\{label}' \{failure}"
    CallBudgetExhausted(label~) =>
      "launch allowance exhausted before agent '\{label}' could launch"
    QuorumNotReached(need~, ok~, of~) =>
      "quorum not reached: \{ok} of \{of} succeeded, \{need} required"
  }
  logger.write_string(text)
}