// session.mbt — upstream `src/session.ts` mirror: `LodySessionMeta` (the
// `_meta.lody` envelope value on standard ACP session updates and
// tool_call/tool_call_update messages), every sub-object schema, and the
// `LodySubagentTask` row used by `_lody/subagents/list`.
//
// `LodySessionMeta` itself has no `version` field upstream; `version: 1`
// lives inside `forkAtTurn` / `activity` / `task`. The elicitation feature
// is a separate envelope (see elicitation.mbt).

///|
/// `LodySteerPromptMeta` — `{ id: string }`, no version. Associates a message
/// chunk with the `_lody/session/steer` request that produced it.
pub(all) struct LodySteerPromptMeta {
  id : String
} derive(Eq, Debug)

///|
pub fn LodySteerPromptMeta::to_json(self : LodySteerPromptMeta) -> Json {
  Json::object(Map::from_array([("id", Json::string(self.id))]))
}

///|
pub fn LodySteerPromptMeta::from_json(
  raw : Json,
) -> Result[LodySteerPromptMeta, String] {
  try {
    let ctx = "lody.steer"
    let fields = object_fields(raw, ctx)
    Ok(LodySteerPromptMeta::{ id: req_string(fields, ctx, "id"), })
  } catch {
    DecodeError::Msg(message) => Err(message)
  }
}

///|
/// `LodyForkAtTurn` — source-turn pointer carried by `session/fork`.
pub(all) struct LodyForkAtTurn {
  version : Int
  turn_id : String?
} derive(Eq, Debug)

///|
pub fn LodyForkAtTurn::to_json(self : LodyForkAtTurn) -> Json {
  let pairs : Array[(String, Json)] = [
    ("version", Json::number(self.version.to_double())),
  ]
  match self.turn_id {
    Some(turn_id) => pairs.push(("turnId", Json::string(turn_id)))
    None => ()
  }
  Json::object(Map::from_array(pairs))
}

///|
pub fn LodyForkAtTurn::from_json(raw : Json) -> Result[LodyForkAtTurn, String] {
  try {
    let ctx = "lody.forkAtTurn"
    let fields = object_fields(raw, ctx)
    let version = req_int(fields, ctx, "version")
    guard version == 1 else {
      raise DecodeError::Msg(
        "\{ctx}: unsupported version \{version} (expected 1)",
      )
    }
    Ok(LodyForkAtTurn::{ version, turn_id: opt_string(fields, ctx, "turnId"), })
  } catch {
    DecodeError::Msg(message) => Err(message)
  }
}

///|
/// `LodyActivityMeta` — compaction/retry lifecycle on tool_call envelopes.
pub(all) struct LodyActivityMeta {
  version : Int
  kind : LodyActivityKind
  automatic : Bool?
  used_tokens_before : Int?
  used_tokens_after : Int?
  duration_ms : Int64?
  failure_reason : String?
} derive(Eq, Debug)

///|
pub fn LodyActivityMeta::to_json(self : LodyActivityMeta) -> Json {
  let pairs : Array[(String, Json)] = [
    ("version", Json::number(self.version.to_double())),
    ("kind", Json::string(self.kind.to_wire())),
  ]
  match self.automatic {
    Some(v) => pairs.push(("automatic", Json::boolean(v)))
    None => ()
  }
  match self.used_tokens_before {
    Some(v) => pairs.push(("usedTokensBefore", Json::number(v.to_double())))
    None => ()
  }
  match self.used_tokens_after {
    Some(v) => pairs.push(("usedTokensAfter", Json::number(v.to_double())))
    None => ()
  }
  match self.duration_ms {
    Some(v) => pairs.push(("durationMs", Json::number(v.to_double())))
    None => ()
  }
  match self.failure_reason {
    Some(v) => pairs.push(("failureReason", Json::string(v)))
    None => ()
  }
  Json::object(Map::from_array(pairs))
}

///|
pub fn LodyActivityMeta::from_json(
  raw : Json,
) -> Result[LodyActivityMeta, String] {
  try {
    let ctx = "lody.activity"
    let fields = object_fields(raw, ctx)
    let version = req_int(fields, ctx, "version")
    guard version == 1 else {
      raise DecodeError::Msg(
        "\{ctx}: unsupported version \{version} (expected 1)",
      )
    }
    Ok(LodyActivityMeta::{
      version,
      kind: wire_of(
        ctx,
        "kind",
        LodyActivityKind::of_wire(req_string(fields, ctx, "kind")),
      ),
      automatic: opt_bool(fields, ctx, "automatic"),
      used_tokens_before: opt_int(fields, ctx, "usedTokensBefore"),
      used_tokens_after: opt_int(fields, ctx, "usedTokensAfter"),
      duration_ms: opt_int64(fields, ctx, "durationMs"),
      failure_reason: opt_string(fields, ctx, "failureReason"),
    })
  } catch {
    DecodeError::Msg(message) => Err(message)
  }
}

///|
/// `LodyTaskMeta.usage` — totals for one delegated task.
pub(all) struct LodyTaskUsage {
  total_tokens : Int?
  tool_uses : Int?
  duration_ms : Int64?
} derive(Eq, Debug)

///|
pub fn LodyTaskUsage::to_json(self : LodyTaskUsage) -> Json {
  let pairs : Array[(String, Json)] = []
  match self.total_tokens {
    Some(v) => pairs.push(("totalTokens", Json::number(v.to_double())))
    None => ()
  }
  match self.tool_uses {
    Some(v) => pairs.push(("toolUses", Json::number(v.to_double())))
    None => ()
  }
  match self.duration_ms {
    Some(v) => pairs.push(("durationMs", Json::number(v.to_double())))
    None => ()
  }
  Json::object(Map::from_array(pairs))
}

///|
pub fn LodyTaskUsage::from_json(raw : Json) -> Result[LodyTaskUsage, String] {
  try {
    let ctx = "lody.task.usage"
    let fields = object_fields(raw, ctx)
    Ok(LodyTaskUsage::{
      total_tokens: opt_int(fields, ctx, "totalTokens"),
      tool_uses: opt_int(fields, ctx, "toolUses"),
      duration_ms: opt_int64(fields, ctx, "durationMs"),
    })
  } catch {
    DecodeError::Msg(message) => Err(message)
  }
}

///|
/// `LodyTaskMeta` — subagent / background / scheduled task lifecycle on
/// tool_call envelopes. The task track: status vocabulary is
/// pending/in_progress/completed/failed (disjoint from the subagent-list
/// vocabulary; see `LodySubagentStatus`).
pub(all) struct LodyTaskMeta {
  version : Int
  task_id : String
  kind : LodyTaskKind
  status : LodyTaskStatus
  description : String?
  actor : String?
  parent_task_id : String?
  parent_tool_call_id : String?
  model_id : String?
  started_at_epoch_seconds : Int64?
  ended_at_epoch_seconds : Int64?
  summary : String?
  error : String?
  last_tool_name : String?
  usage : LodyTaskUsage?
  skip_transcript : Bool?
} derive(Eq, Debug)

///|
pub fn LodyTaskMeta::to_json(self : LodyTaskMeta) -> Json {
  let pairs : Array[(String, Json)] = [
    ("version", Json::number(self.version.to_double())),
    ("taskId", Json::string(self.task_id)),
    ("kind", Json::string(self.kind.to_wire())),
    ("status", Json::string(self.status.to_wire())),
  ]
  match self.description {
    Some(v) => pairs.push(("description", Json::string(v)))
    None => ()
  }
  match self.actor {
    Some(v) => pairs.push(("actor", Json::string(v)))
    None => ()
  }
  match self.parent_task_id {
    Some(v) => pairs.push(("parentTaskId", Json::string(v)))
    None => ()
  }
  match self.parent_tool_call_id {
    Some(v) => pairs.push(("parentToolCallId", Json::string(v)))
    None => ()
  }
  match self.model_id {
    Some(v) => pairs.push(("modelId", Json::string(v)))
    None => ()
  }
  match self.started_at_epoch_seconds {
    Some(v) =>
      pairs.push(("startedAtEpochSeconds", Json::number(v.to_double())))
    None => ()
  }
  match self.ended_at_epoch_seconds {
    Some(v) => pairs.push(("endedAtEpochSeconds", Json::number(v.to_double())))
    None => ()
  }
  match self.summary {
    Some(v) => pairs.push(("summary", Json::string(v)))
    None => ()
  }
  match self.error {
    Some(v) => pairs.push(("error", Json::string(v)))
    None => ()
  }
  match self.last_tool_name {
    Some(v) => pairs.push(("lastToolName", Json::string(v)))
    None => ()
  }
  match self.usage {
    Some(v) => pairs.push(("usage", v.to_json()))
    None => ()
  }
  match self.skip_transcript {
    Some(v) => pairs.push(("skipTranscript", Json::boolean(v)))
    None => ()
  }
  Json::object(Map::from_array(pairs))
}

///|
pub fn LodyTaskMeta::from_json(raw : Json) -> Result[LodyTaskMeta, String] {
  try {
    let ctx = "lody.task"
    let fields = object_fields(raw, ctx)
    let version = req_int(fields, ctx, "version")
    guard version == 1 else {
      raise DecodeError::Msg(
        "\{ctx}: unsupported version \{version} (expected 1)",
      )
    }
    Ok(LodyTaskMeta::{
      version,
      task_id: req_string(fields, ctx, "taskId"),
      kind: wire_of(
        ctx,
        "kind",
        LodyTaskKind::of_wire(req_string(fields, ctx, "kind")),
      ),
      status: wire_of(
        ctx,
        "status",
        LodyTaskStatus::of_wire(req_string(fields, ctx, "status")),
      ),
      description: opt_string(fields, ctx, "description"),
      actor: opt_string(fields, ctx, "actor"),
      parent_task_id: opt_string(fields, ctx, "parentTaskId"),
      parent_tool_call_id: opt_string(fields, ctx, "parentToolCallId"),
      model_id: opt_string(fields, ctx, "modelId"),
      started_at_epoch_seconds: opt_int64(fields, ctx, "startedAtEpochSeconds"),
      ended_at_epoch_seconds: opt_int64(fields, ctx, "endedAtEpochSeconds"),
      summary: opt_string(fields, ctx, "summary"),
      error: opt_string(fields, ctx, "error"),
      last_tool_name: opt_string(fields, ctx, "lastToolName"),
      usage: opt_sub(fields, ctx, "usage", LodyTaskUsage::from_json),
      skip_transcript: opt_bool(fields, ctx, "skipTranscript"),
    })
  } catch {
    DecodeError::Msg(message) => Err(message)
  }
}

///|
/// `LodyGoalSnapshot` — one goal-management snapshot on session updates.
/// `lastReason` / `tokenBudget` are `string | null` / `number | null`
/// upstream without a distinct absent meaning, so both null and absent
/// decode to `None`.
pub(all) struct LodyGoalSnapshot {
  objective : String
  status : LodyGoalStatus
  iterations : Int?
  last_reason : String?
  created_at_epoch_seconds : Int64?
  updated_at_epoch_seconds : Int64?
  token_budget : Int?
  tokens_used : Int?
  time_used_seconds : Int64?
} derive(Eq, Debug)

///|
pub fn LodyGoalSnapshot::to_json(self : LodyGoalSnapshot) -> Json {
  let pairs : Array[(String, Json)] = [
    ("objective", Json::string(self.objective)),
    ("status", Json::string(self.status.to_wire())),
  ]
  match self.iterations {
    Some(v) => pairs.push(("iterations", Json::number(v.to_double())))
    None => ()
  }
  match self.last_reason {
    Some(v) => pairs.push(("lastReason", Json::string(v)))
    None => ()
  }
  match self.created_at_epoch_seconds {
    Some(v) =>
      pairs.push(("createdAtEpochSeconds", Json::number(v.to_double())))
    None => ()
  }
  match self.updated_at_epoch_seconds {
    Some(v) =>
      pairs.push(("updatedAtEpochSeconds", Json::number(v.to_double())))
    None => ()
  }
  match self.token_budget {
    Some(v) => pairs.push(("tokenBudget", Json::number(v.to_double())))
    None => ()
  }
  match self.tokens_used {
    Some(v) => pairs.push(("tokensUsed", Json::number(v.to_double())))
    None => ()
  }
  match self.time_used_seconds {
    Some(v) => pairs.push(("timeUsedSeconds", Json::number(v.to_double())))
    None => ()
  }
  Json::object(Map::from_array(pairs))
}

///|
pub fn LodyGoalSnapshot::from_json(
  raw : Json,
) -> Result[LodyGoalSnapshot, String] {
  try {
    let ctx = "lody.goal"
    let fields = object_fields(raw, ctx)
    Ok(LodyGoalSnapshot::{
      objective: req_string(fields, ctx, "objective"),
      status: wire_of(
        ctx,
        "status",
        LodyGoalStatus::of_wire(req_string(fields, ctx, "status")),
      ),
      iterations: opt_int(fields, ctx, "iterations"),
      last_reason: opt_string(fields, ctx, "lastReason"),
      created_at_epoch_seconds: opt_int64(fields, ctx, "createdAtEpochSeconds"),
      updated_at_epoch_seconds: opt_int64(fields, ctx, "updatedAtEpochSeconds"),
      token_budget: opt_int(fields, ctx, "tokenBudget"),
      tokens_used: opt_int(fields, ctx, "tokensUsed"),
      time_used_seconds: opt_int64(fields, ctx, "timeUsedSeconds"),
    })
  } catch {
    DecodeError::Msg(message) => Err(message)
  }
}

///|
/// `LodyNotice` — one notification on session updates.
pub(all) struct LodyNotice {
  level : LodyNoticeLevel
  message : String
  source : String?
} derive(Eq, Debug)

///|
pub fn LodyNotice::to_json(self : LodyNotice) -> Json {
  let pairs : Array[(String, Json)] = [
    ("level", Json::string(self.level.to_wire())),
    ("message", Json::string(self.message)),
  ]
  match self.source {
    Some(v) => pairs.push(("source", Json::string(v)))
    None => ()
  }
  Json::object(Map::from_array(pairs))
}

///|
pub fn LodyNotice::from_json(raw : Json) -> Result[LodyNotice, String] {
  try {
    let ctx = "lody.notice"
    let fields = object_fields(raw, ctx)
    Ok(LodyNotice::{
      level: wire_of(
        ctx,
        "level",
        LodyNoticeLevel::of_wire(req_string(fields, ctx, "level")),
      ),
      message: req_string(fields, ctx, "message"),
      source: opt_string(fields, ctx, "source"),
    })
  } catch {
    DecodeError::Msg(message) => Err(message)
  }
}

///|
/// Tri-state for `LodySessionMeta.goal` (`LodyGoalSnapshot | null`, or the
/// field absent): absent means "no goal information", explicit null means
/// "the goal was cleared".
pub(all) enum LodyGoalPresence {
  GoalAbsent
  GoalCleared
  GoalSet(LodyGoalSnapshot)
} derive(Eq, Debug)

///|
pub fn LodyGoalPresence::to_json(self : LodyGoalPresence) -> (String, Json)? {
  match self {
    GoalAbsent => None
    GoalCleared => Some(("goal", Json::null()))
    GoalSet(snapshot) => Some(("goal", snapshot.to_json()))
  }
}

///|
pub fn LodyGoalPresence::from_json(
  raw : Json?,
) -> Result[LodyGoalPresence, String] {
  match raw {
    None => Ok(GoalAbsent)
    Some(Json::Null) => Ok(GoalCleared)
    Some(value) =>
      match LodyGoalSnapshot::from_json(value) {
        Ok(snapshot) => Ok(GoalSet(snapshot))
        Err(message) => Err(message)
      }
  }
}

///|
/// `LodySessionMeta` — the full `_meta.lody` envelope value on standard ACP
/// session updates and tool_call/tool_call_update messages. No `version`
/// field: versions live inside `forkAtTurn` / `activity` / `task`.
pub(all) struct LodySessionMeta {
  turn_id : String?
  fork_at_turn : LodyForkAtTurn?
  steer : LodySteerPromptMeta?
  tool_name : String?
  activity : LodyActivityMeta?
  task : LodyTaskMeta?
  goal : LodyGoalPresence
  notice : LodyNotice?
  title_source : LodyTitleSource?
  message_phase : LodyMessagePhase?
} derive(Eq, Debug)

///|
pub fn LodySessionMeta::to_json(self : LodySessionMeta) -> Json {
  let pairs : Array[(String, Json)] = []
  match self.turn_id {
    Some(v) => pairs.push(("turnId", Json::string(v)))
    None => ()
  }
  match self.fork_at_turn {
    Some(v) => pairs.push(("forkAtTurn", v.to_json()))
    None => ()
  }
  match self.steer {
    Some(v) => pairs.push(("steer", v.to_json()))
    None => ()
  }
  match self.tool_name {
    Some(v) => pairs.push(("toolName", Json::string(v)))
    None => ()
  }
  match self.activity {
    Some(v) => pairs.push(("activity", v.to_json()))
    None => ()
  }
  match self.task {
    Some(v) => pairs.push(("task", v.to_json()))
    None => ()
  }
  match self.goal.to_json() {
    Some(pair) => pairs.push(pair)
    None => ()
  }
  match self.notice {
    Some(v) => pairs.push(("notice", v.to_json()))
    None => ()
  }
  match self.title_source {
    Some(v) => pairs.push(("titleSource", Json::string(v.to_wire())))
    None => ()
  }
  match self.message_phase {
    Some(v) => pairs.push(("messagePhase", Json::string(v.to_wire())))
    None => ()
  }
  Json::object(Map::from_array(pairs))
}

///|
pub fn LodySessionMeta::from_json(
  raw : Json,
) -> Result[LodySessionMeta, String] {
  try {
    let ctx = "lody.sessionMeta"
    let fields = object_fields(raw, ctx)
    let turn_id = opt_string(fields, ctx, "turnId")
    let fork_at_turn = opt_sub(
      fields,
      ctx,
      "forkAtTurn",
      LodyForkAtTurn::from_json,
    )
    let steer = opt_sub(fields, ctx, "steer", LodySteerPromptMeta::from_json)
    let tool_name = opt_string(fields, ctx, "toolName")
    let activity = opt_sub(fields, ctx, "activity", LodyActivityMeta::from_json)
    let task = opt_sub(fields, ctx, "task", LodyTaskMeta::from_json)
    let goal : LodyGoalPresence = match
      LodyGoalPresence::from_json(fields.get("goal")) {
      Ok(g) => g
      Err(message) => raise DecodeError::Msg(message)
    }
    let notice = opt_sub(fields, ctx, "notice", LodyNotice::from_json)
    let title_source = opt_wire(
      ctx,
      "titleSource",
      opt_string(fields, ctx, "titleSource"),
      LodyTitleSource::of_wire,
    )
    let message_phase = opt_wire(
      ctx,
      "messagePhase",
      opt_string(fields, ctx, "messagePhase"),
      LodyMessagePhase::of_wire,
    )
    Ok(LodySessionMeta::{
      turn_id,
      fork_at_turn,
      steer,
      tool_name,
      activity,
      task,
      goal,
      notice,
      title_source,
      message_phase,
    })
  } catch {
    DecodeError::Msg(message) => Err(message)
  }
}

///|
/// `LodySubagentTask` — one row of `_lody/subagents/list`. Status vocabulary
/// (running/completed/failed/timed_out/killed/lost) is disjoint from
/// `LodyTaskStatus`. `endedAtEpochSeconds` is required but nullable upstream,
/// so `to_json` always emits the key (null until the task ends).
pub(all) struct LodySubagentTask {
  task_id : String
  description : String
  status : LodySubagentStatus
  agent_id : String?
  subagent_type : String?
  model_id : String?
  thinking_effort : String?
  started_at_epoch_seconds : Int64
  ended_at_epoch_seconds : Int64?
  stop_reason : String?
} derive(Eq, Debug)

///|
pub fn LodySubagentTask::to_json(self : LodySubagentTask) -> Json {
  let pairs : Array[(String, Json)] = [
    ("taskId", Json::string(self.task_id)),
    ("description", Json::string(self.description)),
    ("status", Json::string(self.status.to_wire())),
  ]
  match self.agent_id {
    Some(v) => pairs.push(("agentId", Json::string(v)))
    None => ()
  }
  match self.subagent_type {
    Some(v) => pairs.push(("subagentType", Json::string(v)))
    None => ()
  }
  match self.model_id {
    Some(v) => pairs.push(("modelId", Json::string(v)))
    None => ()
  }
  match self.thinking_effort {
    Some(v) => pairs.push(("thinkingEffort", Json::string(v)))
    None => ()
  }
  pairs.push(
    (
      "startedAtEpochSeconds",
      Json::number(self.started_at_epoch_seconds.to_double()),
    ),
  )
  match self.ended_at_epoch_seconds {
    Some(v) => pairs.push(("endedAtEpochSeconds", Json::number(v.to_double())))
    None => pairs.push(("endedAtEpochSeconds", Json::null()))
  }
  match self.stop_reason {
    Some(v) => pairs.push(("stopReason", Json::string(v)))
    None => ()
  }
  Json::object(Map::from_array(pairs))
}

///|
pub fn LodySubagentTask::from_json(
  raw : Json,
) -> Result[LodySubagentTask, String] {
  try {
    let ctx = "lody.subagents.task"
    let fields = object_fields(raw, ctx)
    Ok(LodySubagentTask::{
      task_id: req_string(fields, ctx, "taskId"),
      description: req_string(fields, ctx, "description"),
      status: wire_of(
        ctx,
        "status",
        LodySubagentStatus::of_wire(req_string(fields, ctx, "status")),
      ),
      agent_id: opt_string(fields, ctx, "agentId"),
      subagent_type: opt_string(fields, ctx, "subagentType"),
      model_id: opt_string(fields, ctx, "modelId"),
      thinking_effort: opt_string(fields, ctx, "thinkingEffort"),
      started_at_epoch_seconds: req_int64(fields, ctx, "startedAtEpochSeconds"),
      ended_at_epoch_seconds: req_int64_nullable(
        fields, ctx, "endedAtEpochSeconds",
      ),
      stop_reason: opt_string(fields, ctx, "stopReason"),
    })
  } catch {
    DecodeError::Msg(message) => Err(message)
  }
}