///|
/// Canonical run/turn/effect identity and cancel contracts (M1, ADR §2.5,
/// §2.6, §2.9). These types are part of the public wire contract: advanced
/// runtimes correlate effect execution and cancellation with them, while the
/// reducer remains their sole allocator.
///
/// Promoted from `internal/kernel_exec`/`internal/puppetry` so the public
/// runtime seam (`colmugx/posoco/runtime`) can name effect/run/turn identity
/// without importing internal packages. Internal packages alias these
/// definitions — this file is the single source of truth.

///|
/// Stable identifier for a single Run. Supplied by the host in the `Start`
/// input and carried through every state.
pub(all) struct RunId {
  value : String
} derive(Eq, Hash, Debug)

///|
pub fn RunId::new(value : String) -> Result[RunId, String] {
  if value == "" {
    Err("RunId must be non-empty")
  } else {
    Ok(RunId::{ value, })
  }
}

///|
pub fn RunId::unchecked(value : String) -> RunId {
  RunId::{ value, }
}

///|
pub fn RunId::to_string(self : RunId) -> String {
  self.value
}

///|
pub impl Show for RunId with fn to_string(self : RunId) -> String {
  "RunId(\{self.value})"
}

///|
/// Identifier for a single turn within a Run. A Run may contain multiple
/// turns (initial prompt + follow-ups); turns never outlive their Run.
/// follow_up allocates a new TurnId under the same RunId.
pub(all) struct TurnId {
  value : String
} derive(Eq, Hash, Debug)

///|
pub fn TurnId::new(value : String) -> Result[TurnId, String] {
  if value == "" {
    Err("TurnId must be non-empty")
  } else {
    Ok(TurnId::{ value, })
  }
}

///|
pub fn TurnId::unchecked(value : String) -> TurnId {
  TurnId::{ value, }
}

///|
pub fn TurnId::to_string(self : TurnId) -> String {
  self.value
}

///|
pub impl Show for TurnId with fn to_string(self : TurnId) -> String {
  self.value
}

///|
/// Stable identifier for a product session. The Kernel never opens or
/// materializes session storage; it carries this identity through effects,
/// journal entries, and committed event envelopes.
pub(all) struct SessionId {
  value : String
} derive(Eq, Hash, Debug)

///|
pub fn SessionId::new(value : String) -> Result[SessionId, String] {
  if value == "" {
    Err("SessionId must be non-empty")
  } else {
    Ok(SessionId::{ value, })
  }
}

///|
pub fn SessionId::unchecked(value : String) -> SessionId {
  SessionId::{ value, }
}

///|
pub fn SessionId::to_string(self : SessionId) -> String {
  self.value
}

///|
/// Identifier for a single declared Effect. Monotonic per Run. Each completion
/// input must correlate to a pending effect with this id; a mismatch is
/// `InvariantViolation::StaleCorrelation`.
pub(all) struct EffectId {
  value : Int
} derive(Eq, Hash, Debug)

///|
pub fn EffectId::first() -> EffectId {
  { value: 1 }
}

///|
pub fn EffectId::next(self : EffectId) -> EffectId {
  { value: self.value + 1 }
}

///|
pub fn EffectId::to_int(self : EffectId) -> Int {
  self.value
}

///|
pub impl Show for EffectId with fn to_string(self : EffectId) -> String {
  "EffectId(\{self.value})"
}

///|
/// Catalog version — bumped every time a snapshot is built. The reducer and
/// executor correlate `CallModel` and `ExecuteTool` effects back to the exact
/// snapshot version the model was prompted against; a mismatch is a kernel
/// invariant failure (`InvariantViolation::StaleSnapshot`).
pub(all) struct CatalogVersion {
  value : Int
} derive(Eq, Debug)

///|
pub fn CatalogVersion::CatalogVersion(value : Int) -> CatalogVersion {
  CatalogVersion::{ value, }
}

///|
pub fn CatalogVersion::to_int(self : CatalogVersion) -> Int {
  self.value
}

///|
pub impl Show for CatalogVersion with fn to_string(self : CatalogVersion) -> String {
  "CatalogVersion(\{self.value})"
}

///|
/// Why a Run was cancelled.
pub(all) enum CancelReason {
  /// Host explicitly cancelled.
  HostRequested(detail~ : String?)
  /// Adapter/host observed the deadline.
  DeadlineReached
} derive(Eq, Debug)

///|
fn detail_or_empty(detail : String?) -> String {
  match detail {
    Some(s) => s
    None => ""
  }
}

///|
pub impl Show for CancelReason with fn to_string(self : CancelReason) -> String {
  match self {
    HostRequested(detail~) => "HostRequested(\{detail_or_empty(detail)})"
    DeadlineReached => "DeadlineReached"
  }
}

///|
/// What an adapter reports after a `CancelOutstanding` effect. The reducer
/// never waits for this.
pub(all) enum CancelDisposition {
  /// Cancel signal propagated.
  Propagated
  /// Cancel not propagated; only stops the reducer waiting.
  NotPropagated
  /// Effect already settled before cancel arrived.
  AlreadySettled
} derive(Eq, Debug)

///|
pub impl Show for CancelDisposition with fn to_string(self : CancelDisposition) -> String {
  match self {
    Propagated => "Propagated"
    NotPropagated => "NotPropagated"
    AlreadySettled => "AlreadySettled"
  }
}

///|
/// Invocation identity for a single model-side call (`ModelPort::chat` /
/// `ModelPort::compact`, and their `Runtime` correspondences). Constructed by
/// posoco at the effect-interpretation layer and passed down unchanged; ports
/// and hosts MUST treat it as read-only.
///
/// This is the canonical answer to "which session/run is this call serving":
/// adapters that key behaviour on session identity (continuity, telemetry,
/// cost attribution, per-session policy) read it from here instead of
/// smuggling it through messages or config.
///
/// Invariants:
/// - `session_id` / `run_id` always identify the Run that issued the call,
///   including `compact` (compact is host-driven but still scoped to a Run).
/// - `effect_id` is `Some` iff the call executes a reducer-allocated
///   `CallModel` effect; it is `None` for `compact`, which is not an effect.
///   Hosts that propagate cancellation key in-flight model calls by this id,
///   mirroring the tool side's `EffectContext.effect_id`.
pub(all) struct InvocationScope {
  session_id : SessionId
  run_id : RunId
  effect_id : EffectId?
} derive(Eq, Debug)