///|
/// Priority of a plan entry.
pub(all) enum PlanEntryPriority {
  High
  Medium
  Low
} derive(Eq, Debug)

///|
/// Lifecycle status of a plan entry.
pub(all) enum PlanEntryStatus {
  Pending
  InProgress
  Completed
} derive(Eq, Debug)

///|
/// One item in an execution plan.
pub(all) struct PlanEntry {
  content : String
  priority : PlanEntryPriority
  status : PlanEntryStatus
  meta : ProtocolNullable[Json]
} derive(Eq, Debug)

///|
/// A complete replacement execution plan.
pub(all) struct Plan {
  entries : Array[PlanEntry]
  meta : ProtocolNullable[Json]
} derive(Eq, Debug)

///|
/// Cumulative cost information.
pub(all) struct Cost {
  amount : Double
  currency : String
  meta : ProtocolNullable[Json]
} derive(Eq, Debug)

///|
/// Current context-window usage.
pub(all) struct UsageUpdate {
  used : UInt64
  size : UInt64
  cost : ProtocolNullable[Cost]
  meta : ProtocolNullable[Json]
} derive(Eq, Debug)

///|
/// The unstructured command input form.
pub(all) struct UnstructuredCommandInput {
  hint : String
  meta : ProtocolNullable[Json]
} derive(Eq, Debug)

///|
/// Available command input variants.
pub(all) enum AvailableCommandInput {
  Unstructured(UnstructuredCommandInput)
} derive(Eq, Debug)

///|
/// One command exposed by an agent.
pub(all) struct AvailableCommand {
  name : String
  description : String
  input : ProtocolNullable[AvailableCommandInput]
  meta : ProtocolNullable[Json]
} derive(Eq, Debug)

///|
/// Complete replacement list of available commands.
pub(all) struct AvailableCommandsUpdate {
  available_commands : Array[AvailableCommand]
  meta : ProtocolNullable[Json]
} derive(Eq, Debug)

///|
/// Current mode selected for a session.
pub(all) struct CurrentModeUpdate {
  current_mode_id : SessionModeId
  meta : ProtocolNullable[Json]
} derive(Eq, Debug)

///|
/// Semantic categories for configuration selectors.
pub(all) enum SessionConfigOptionCategory {
  Mode
  Model
  ModelConfig
  ThoughtLevel
  Other(String)
} derive(Eq, Debug)

///|
/// One selectable configuration value.
pub(all) struct SessionConfigSelectOption {
  value : SessionConfigValueId
  name : String
  description : ProtocolNullable[String]
  meta : ProtocolNullable[Json]
} derive(Eq, Debug)

///|
/// A grouped set of selectable configuration values.
pub(all) struct SessionConfigSelectGroup {
  group : SessionConfigGroupId
  name : String
  options : Array[SessionConfigSelectOption]
  meta : ProtocolNullable[Json]
} derive(Eq, Debug)

///|
/// Flat or grouped selectable values.
pub(all) enum SessionConfigSelectOptions {
  Ungrouped(Array[SessionConfigSelectOption])
  Grouped(Array[SessionConfigSelectGroup])
} derive(Eq, Debug)

///|
/// Selector-specific configuration payload.
pub(all) struct SessionConfigSelect {
  current_value : SessionConfigValueId
  options : SessionConfigSelectOptions
} derive(Eq, Debug)

///|
/// Boolean-specific configuration payload.
pub(all) struct SessionConfigBoolean {
  current_value : Bool
} derive(Eq, Debug)

///|
/// Configuration option payload variants.
pub(all) enum SessionConfigOptionKind {
  Select(SessionConfigSelect)
  Boolean(SessionConfigBoolean)
} derive(Eq, Debug)

///|
/// One complete session configuration option.
pub(all) struct SessionConfigOption {
  id : SessionConfigId
  name : String
  description : ProtocolNullable[String]
  category : ProtocolNullable[SessionConfigOptionCategory]
  kind : SessionConfigOptionKind
  meta : ProtocolNullable[Json]
} derive(Eq, Debug)

///|
/// Complete replacement configuration option list.
pub(all) struct ConfigOptionUpdate {
  config_options : Array[SessionConfigOption]
  meta : ProtocolNullable[Json]
} derive(Eq, Debug)

///|
/// Partial session metadata update.
pub(all) struct SessionInfoUpdate {
  title : ProtocolNullable[String]
  updated_at : ProtocolNullable[String]
  meta : ProtocolNullable[Json]
} derive(Eq, Debug)

///|
/// Session metadata used by session listing.
pub(all) struct SessionInfo {
  session_id : SessionId
  cwd : String
  additional_directories : ProtocolNullable[Array[String]]
  title : ProtocolNullable[String]
  updated_at : ProtocolNullable[String]
  meta : ProtocolNullable[Json]
} derive(Eq, Debug)

///|
/// All stable v1 session update variants.
pub(all) enum SessionUpdate {
  UserMessageChunk(ContentChunk)
  AgentMessageChunk(ContentChunk)
  AgentThoughtChunk(ContentChunk)
  ToolCall(ToolCall)
  ToolCallUpdate(ToolCallUpdate)
  Plan(Plan)
  AvailableCommandsUpdate(AvailableCommandsUpdate)
  CurrentModeUpdate(CurrentModeUpdate)
  ConfigOptionUpdate(ConfigOptionUpdate)
  SessionInfoUpdate(SessionInfoUpdate)
  UsageUpdate(UsageUpdate)
} derive(Eq, Debug)

///|
fn encode_plan_priority(value : PlanEntryPriority) -> Json {
  Json::string(
    match value {
      High => "high"
      Medium => "medium"
      Low => "low"
    },
  )
}

///|
fn decode_plan_priority(
  value : Json,
  path~ : String,
) -> PlanEntryPriority raise ProtocolDecodeError {
  match value {
    String("high") => High
    String("medium") => Medium
    String("low") => Low
    String(value) => raise InvalidDiscriminator(path~, value~)
    _ => raise ExpectedString(path~)
  }
}

///|
fn encode_plan_status(value : PlanEntryStatus) -> Json {
  Json::string(
    match value {
      Pending => "pending"
      InProgress => "in_progress"
      Completed => "completed"
    },
  )
}

///|
fn decode_plan_status(
  value : Json,
  path~ : String,
) -> PlanEntryStatus raise ProtocolDecodeError {
  match value {
    String("pending") => Pending
    String("in_progress") => InProgress
    String("completed") => Completed
    String(value) => raise InvalidDiscriminator(path~, value~)
    _ => raise ExpectedString(path~)
  }
}

///|
fn encode_plan_entry(value : PlanEntry) -> Json raise ProtocolDecodeError {
  let fields : Map[String, Json] = Map([])
  fields["content"] = Json::string(value.content)
  fields["priority"] = encode_plan_priority(value.priority)
  fields["status"] = encode_plan_status(value.status)
  protocol_put_meta(fields, value.meta)
  Json::object(fields)
}

///|
fn decode_plan_entry(
  value : Json,
  path~ : String,
) -> PlanEntry raise ProtocolDecodeError {
  let fields = protocol_require_object(value, path~)
  protocol_reject_unknown(
    fields,
    ["content", "priority", "status", "_meta"],
    prefix=path,
  )
  let content = protocol_required_string(
    fields,
    "content",
    path=path + ".content",
  )
  let priority = decode_plan_priority(
    protocol_required(fields, "priority", path=path + ".priority"),
    path=path + ".priority",
  )
  let status = decode_plan_status(
    protocol_required(fields, "status", path=path + ".status"),
    path=path + ".status",
  )
  let meta = protocol_meta(fields, path=path + "._meta")
  { content, priority, status, meta }
}

///|
fn encode_plan_entries(
  values : Array[PlanEntry],
) -> Json raise ProtocolDecodeError {
  Json::array(values.map(value => encode_plan_entry(value)))
}

///|
fn decode_plan_entries(
  value : Json,
  path~ : String,
) -> Array[PlanEntry] raise ProtocolDecodeError {
  let values = protocol_require_array(value, path~)
  let entries : Array[PlanEntry] = []
  for index, value in values {
    entries.push(
      decode_plan_entry(value, path=path + "[" + index.to_string() + "]"),
    )
  }
  entries
}

///|
/// Decode a plan update from JSON.
pub fn plan_from_json(
  value : Json,
  path? : String = "plan",
) -> Plan raise ProtocolDecodeError {
  let fields = protocol_require_object(value, path~)
  protocol_reject_unknown(fields, ["entries", "_meta"], prefix=path)
  let entries = match fields.get("entries") {
    Some(value) => decode_plan_entries(value, path=path + ".entries")
    None => raise MissingField(path=path + ".entries")
  }
  let meta = protocol_meta(fields, path=path + "._meta")
  { entries, meta }
}

///|
/// Encode a plan update as JSON.
pub fn plan_to_json(value : Plan) -> Json raise ProtocolDecodeError {
  let fields : Map[String, Json] = Map([])
  fields["entries"] = encode_plan_entries(value.entries)
  protocol_put_meta(fields, value.meta)
  Json::object(fields)
}

///|
fn encode_cost(value : Cost) -> Json raise ProtocolDecodeError {
  let fields : Map[String, Json] = Map([])
  fields["amount"] = encode_finite_number(value.amount, path="amount")
  fields["currency"] = Json::string(value.currency)
  protocol_put_meta(fields, value.meta)
  Json::object(fields)
}

///|
fn decode_cost(value : Json, path~ : String) -> Cost raise ProtocolDecodeError {
  let fields = protocol_require_object(value, path~)
  protocol_reject_unknown(fields, ["amount", "currency", "_meta"], prefix=path)
  let amount = decode_finite_number(
    protocol_required(fields, "amount", path=path + ".amount"),
    path=path + ".amount",
  )
  let currency = protocol_required_string(
    fields,
    "currency",
    path=path + ".currency",
  )
  let meta = protocol_meta(fields, path=path + "._meta")
  { amount, currency, meta }
}

///|
fn put_nullable_cost(
  fields : Map[String, Json],
  value : ProtocolNullable[Cost],
) -> Unit raise ProtocolDecodeError {
  match value {
    Omitted => ()
    Null => fields["cost"] = Json::null()
    Value(value) => fields["cost"] = encode_cost(value)
  }
}

///|
/// Decode a usage update from JSON.
pub fn usage_update_from_json(
  value : Json,
  path? : String = "usage",
) -> UsageUpdate raise ProtocolDecodeError {
  let fields = protocol_require_object(value, path~)
  protocol_reject_unknown(
    fields,
    ["used", "size", "cost", "_meta"],
    prefix=path,
  )
  let used = protocol_decode_uint64(
    protocol_required(fields, "used", path=path + ".used"),
    path=path + ".used",
  )
  let size = protocol_decode_uint64(
    protocol_required(fields, "size", path=path + ".size"),
    path=path + ".size",
  )
  let cost : ProtocolNullable[Cost] = match protocol_field(fields, "cost") {
    Omitted => Omitted
    Null => Null
    Value(value) => Value(decode_cost(value, path=path + ".cost"))
  }
  let meta = protocol_meta(fields, path=path + "._meta")
  { used, size, cost, meta }
}

///|
/// Encode a usage update as JSON.
pub fn usage_update_to_json(
  value : UsageUpdate,
) -> Json raise ProtocolDecodeError {
  let fields : Map[String, Json] = Map([])
  fields["used"] = value.used.to_json()
  fields["size"] = value.size.to_json()
  put_nullable_cost(fields, value.cost)
  protocol_put_meta(fields, value.meta)
  Json::object(fields)
}

///|
fn encode_unstructured_input(
  value : UnstructuredCommandInput,
) -> Json raise ProtocolDecodeError {
  let fields : Map[String, Json] = Map([])
  fields["hint"] = Json::string(value.hint)
  protocol_put_meta(fields, value.meta)
  Json::object(fields)
}

///|
fn decode_unstructured_input(
  value : Json,
  path~ : String,
) -> UnstructuredCommandInput raise ProtocolDecodeError {
  let fields = protocol_require_object(value, path~)
  protocol_reject_unknown(fields, ["hint", "_meta"], prefix=path)
  let hint = protocol_required_string(fields, "hint", path=path + ".hint")
  let meta = protocol_meta(fields, path=path + "._meta")
  { hint, meta }
}

///|
fn encode_command_input(
  value : AvailableCommandInput,
) -> Json raise ProtocolDecodeError {
  match value {
    Unstructured(value) => encode_unstructured_input(value)
  }
}

///|
fn decode_command_input(
  value : Json,
  path~ : String,
) -> AvailableCommandInput raise ProtocolDecodeError {
  Unstructured(decode_unstructured_input(value, path~))
}

///|
fn encode_commands(
  values : Array[AvailableCommand],
) -> Json raise ProtocolDecodeError {
  Json::array(
    values.map(value => {
      let fields : Map[String, Json] = Map([])
      fields["name"] = Json::string(value.name)
      fields["description"] = Json::string(value.description)
      match value.input {
        Omitted => ()
        Null => fields["input"] = Json::null()
        Value(input) => fields["input"] = encode_command_input(input)
      }
      protocol_put_meta(fields, value.meta)
      Json::object(fields)
    }),
  )
}

///|
fn decode_commands(
  value : Json,
  path~ : String,
) -> Array[AvailableCommand] raise ProtocolDecodeError {
  let values = protocol_require_array(value, path~)
  let commands : Array[AvailableCommand] = []
  for index, value in values {
    let command_path = path + "[" + index.to_string() + "]"
    let fields = protocol_require_object(value, path=command_path)
    protocol_reject_unknown(
      fields,
      ["name", "description", "input", "_meta"],
      prefix=command_path,
    )
    let name = protocol_required_string(
      fields,
      "name",
      path=command_path + ".name",
    )
    let description = protocol_required_string(
      fields,
      "description",
      path=command_path + ".description",
    )
    let input : ProtocolNullable[AvailableCommandInput] = match
      protocol_field(fields, "input") {
      Omitted => Omitted
      Null => Null
      Value(value) =>
        Value(decode_command_input(value, path=command_path + ".input"))
    }
    let meta = protocol_meta(fields, path=command_path + "._meta")
    commands.push({ name, description, input, meta })
  }
  commands
}

///|
/// Decode an available-commands update from JSON.
pub fn available_commands_update_from_json(
  value : Json,
  path? : String = "availableCommands",
) -> AvailableCommandsUpdate raise ProtocolDecodeError {
  let fields = protocol_require_object(value, path~)
  protocol_reject_unknown(fields, ["availableCommands", "_meta"], prefix=path)
  let available_commands = match fields.get("availableCommands") {
    Some(value) => decode_commands(value, path=path + ".availableCommands")
    None => raise MissingField(path=path + ".availableCommands")
  }
  let meta = protocol_meta(fields, path=path + "._meta")
  { available_commands, meta }
}

///|
/// Encode an available-commands update as JSON.
pub fn available_commands_update_to_json(
  value : AvailableCommandsUpdate,
) -> Json raise ProtocolDecodeError {
  let fields : Map[String, Json] = Map([])
  fields["availableCommands"] = encode_commands(value.available_commands)
  protocol_put_meta(fields, value.meta)
  Json::object(fields)
}

///|
fn encode_config_category(value : SessionConfigOptionCategory) -> Json {
  Json::string(
    match value {
      Mode => "mode"
      Model => "model"
      ModelConfig => "model_config"
      ThoughtLevel => "thought_level"
      Other(value) => value
    },
  )
}

///|
fn decode_config_category(
  value : Json,
  path~ : String,
) -> SessionConfigOptionCategory raise ProtocolDecodeError {
  match value {
    String("mode") => Mode
    String("model") => Model
    String("model_config") => ModelConfig
    String("thought_level") => ThoughtLevel
    String(value) => Other(value)
    _ => raise ExpectedString(path~)
  }
}

///|
fn encode_select_option(
  value : SessionConfigSelectOption,
) -> Json raise ProtocolDecodeError {
  let fields : Map[String, Json] = Map([])
  fields["value"] = Json::string(value.value)
  fields["name"] = Json::string(value.name)
  protocol_put_nullable(fields, "description", value.description, value => {
    Json::string(value)
  })
  protocol_put_meta(fields, value.meta)
  Json::object(fields)
}

///|
fn decode_select_option(
  value : Json,
  path~ : String,
) -> SessionConfigSelectOption raise ProtocolDecodeError {
  let fields = protocol_require_object(value, path~)
  protocol_reject_unknown(
    fields,
    ["value", "name", "description", "_meta"],
    prefix=path,
  )
  let option_value = protocol_required_string(
    fields,
    "value",
    path=path + ".value",
  )
  let name = protocol_required_string(fields, "name", path=path + ".name")
  let description = protocol_nullable_string(
    fields,
    "description",
    path=path + ".description",
  )
  let meta = protocol_meta(fields, path=path + "._meta")
  { value: option_value, name, description, meta }
}

///|
fn encode_select_options(
  values : SessionConfigSelectOptions,
) -> Json raise ProtocolDecodeError {
  match values {
    Ungrouped(values) =>
      Json::array(values.map(value => encode_select_option(value)))
    Grouped(values) =>
      Json::array(values.map(value => encode_select_group(value)))
  }
}

///|
fn decode_select_options(
  value : Json,
  path~ : String,
) -> SessionConfigSelectOptions raise ProtocolDecodeError {
  let values = protocol_require_array(value, path~)
  if values.length() == 0 {
    Ungrouped([])
  } else {
    let first = protocol_require_object(values[0], path=path + "[0]")
    let has_value = first.contains("value")
    let has_group = first.contains("group")
    if has_value && has_group {
      raise InvalidField(
        path~,
        reason="select option cannot be both grouped and ungrouped",
      )
    }
    if has_value {
      let options : Array[SessionConfigSelectOption] = []
      for index, item in values {
        let item_path = path + "[" + index.to_string() + "]"
        let item_fields = protocol_require_object(item, path=item_path)
        if !item_fields.contains("value") || item_fields.contains("group") {
          raise InvalidField(path=item_path, reason="mixed select option forms")
        }
        options.push(decode_select_option(item, path=item_path))
      }
      Ungrouped(options)
    } else if has_group {
      let groups : Array[SessionConfigSelectGroup] = []
      for index, item in values {
        let item_path = path + "[" + index.to_string() + "]"
        let item_fields = protocol_require_object(item, path=item_path)
        if !item_fields.contains("group") || item_fields.contains("value") {
          raise InvalidField(
            path=item_path,
            reason="mixed select option group forms",
          )
        }
        groups.push(decode_select_group(item, path=item_path))
      }
      Grouped(groups)
    } else {
      raise InvalidField(
        path~,
        reason="select option must contain value or group",
      )
    }
  }
}

///|
fn encode_select_group(
  value : SessionConfigSelectGroup,
) -> Json raise ProtocolDecodeError {
  let fields : Map[String, Json] = Map([])
  fields["group"] = Json::string(value.group)
  fields["name"] = Json::string(value.name)
  fields["options"] = Json::array(
    value.options.map(value => encode_select_option(value)),
  )
  protocol_put_meta(fields, value.meta)
  Json::object(fields)
}

///|
fn decode_select_group(
  value : Json,
  path~ : String,
) -> SessionConfigSelectGroup raise ProtocolDecodeError {
  let fields = protocol_require_object(value, path~)
  protocol_reject_unknown(
    fields,
    ["group", "name", "options", "_meta"],
    prefix=path,
  )
  let group = protocol_required_string(fields, "group", path=path + ".group")
  let name = protocol_required_string(fields, "name", path=path + ".name")
  let options = match fields.get("options") {
    Some(value) => {
      let values = protocol_require_array(value, path=path + ".options")
      let result : Array[SessionConfigSelectOption] = []
      for index, value in values {
        result.push(
          decode_select_option(
            value,
            path=path + ".options[" + index.to_string() + "]",
          ),
        )
      }
      result
    }
    None => raise MissingField(path=path + ".options")
  }
  let meta = protocol_meta(fields, path=path + "._meta")
  { group, name, options, meta }
}

///|
fn encode_config_option(
  value : SessionConfigOption,
) -> Json raise ProtocolDecodeError {
  let fields : Map[String, Json] = Map([])
  fields["id"] = Json::string(value.id)
  fields["name"] = Json::string(value.name)
  protocol_put_nullable(fields, "description", value.description, value => {
    Json::string(value)
  })
  protocol_put_nullable(
    fields,
    "category",
    value.category,
    encode_config_category,
  )
  match value.kind {
    Select(select) => {
      fields["type"] = Json::string("select")
      fields["currentValue"] = Json::string(select.current_value)
      fields["options"] = encode_select_options(select.options)
    }
    Boolean(boolean) => {
      fields["type"] = Json::string("boolean")
      fields["currentValue"] = Json::boolean(boolean.current_value)
    }
  }
  protocol_put_meta(fields, value.meta)
  Json::object(fields)
}

///|
fn decode_config_option(
  value : Json,
  path~ : String,
) -> SessionConfigOption raise ProtocolDecodeError {
  let fields = protocol_require_object(value, path~)
  let kind_name = protocol_required_string(fields, "type", path=path + ".type")
  let allowed = match kind_name {
    "select" =>
      [
        "id", "name", "description", "category", "type", "currentValue", "options",
        "_meta",
      ]
    "boolean" =>
      ["id", "name", "description", "category", "type", "currentValue", "_meta"]
    _ => ["type"]
  }
  if kind_name != "select" && kind_name != "boolean" {
    raise InvalidDiscriminator(path=path + ".type", value=kind_name)
  }
  protocol_reject_unknown(fields, allowed, prefix=path)
  let id = protocol_required_string(fields, "id", path=path + ".id")
  let name = protocol_required_string(fields, "name", path=path + ".name")
  let description = protocol_nullable_string(
    fields,
    "description",
    path=path + ".description",
  )
  let category : ProtocolNullable[SessionConfigOptionCategory] = match
    protocol_field(fields, "category") {
    Omitted => Omitted
    Null => Null
    Value(value) =>
      Value(decode_config_category(value, path=path + ".category"))
  }
  let kind = if kind_name == "select" {
    let current_value = protocol_required_string(
      fields,
      "currentValue",
      path=path + ".currentValue",
    )
    let options = match fields.get("options") {
      Some(value) => decode_select_options(value, path=path + ".options")
      None => raise MissingField(path=path + ".options")
    }
    Select({ current_value, options })
  } else {
    let current_value = protocol_decode_boolean(
      protocol_required(fields, "currentValue", path=path + ".currentValue"),
      path=path + ".currentValue",
    )
    Boolean({ current_value, })
  }
  let meta = protocol_meta(fields, path=path + "._meta")
  { id, name, description, category, kind, meta }
}

///|
fn encode_config_options(
  values : Array[SessionConfigOption],
) -> Json raise ProtocolDecodeError {
  Json::array(
    values.map(fn(value) raise ProtocolDecodeError {
      encode_config_option(value)
    }),
  )
}

///|
fn decode_config_options(
  value : Json,
  path~ : String,
) -> Array[SessionConfigOption] raise ProtocolDecodeError {
  let values = protocol_require_array(value, path~)
  let options : Array[SessionConfigOption] = []
  for index, value in values {
    options.push(
      decode_config_option(value, path=path + "[" + index.to_string() + "]"),
    )
  }
  options
}

///|
/// Decode a current-mode update from JSON.
pub fn current_mode_update_from_json(
  value : Json,
  path? : String = "currentMode",
) -> CurrentModeUpdate raise ProtocolDecodeError {
  let fields = protocol_require_object(value, path~)
  protocol_reject_unknown(fields, ["currentModeId", "_meta"], prefix=path)
  let current_mode_id = protocol_required_string(
    fields,
    "currentModeId",
    path=path + ".currentModeId",
  )
  let meta = protocol_meta(fields, path=path + "._meta")
  { current_mode_id, meta }
}

///|
/// Encode a current-mode update as JSON.
pub fn current_mode_update_to_json(
  value : CurrentModeUpdate,
) -> Json raise ProtocolDecodeError {
  let fields : Map[String, Json] = Map([])
  fields["currentModeId"] = Json::string(value.current_mode_id)
  protocol_put_meta(fields, value.meta)
  Json::object(fields)
}

///|
/// Decode a configuration-options update from JSON.
pub fn config_option_update_from_json(
  value : Json,
  path? : String = "configOptions",
) -> ConfigOptionUpdate raise ProtocolDecodeError {
  let fields = protocol_require_object(value, path~)
  protocol_reject_unknown(fields, ["configOptions", "_meta"], prefix=path)
  let config_options = match fields.get("configOptions") {
    Some(value) => decode_config_options(value, path=path + ".configOptions")
    None => raise MissingField(path=path + ".configOptions")
  }
  let meta = protocol_meta(fields, path=path + "._meta")
  { config_options, meta }
}

///|
/// Encode a configuration-options update as JSON.
pub fn config_option_update_to_json(
  value : ConfigOptionUpdate,
) -> Json raise ProtocolDecodeError {
  let fields : Map[String, Json] = Map([])
  fields["configOptions"] = encode_config_options(value.config_options)
  protocol_put_meta(fields, value.meta)
  Json::object(fields)
}

///|
/// Decode a session-information update from JSON.
pub fn session_info_update_from_json(
  value : Json,
  path? : String = "sessionInfo",
) -> SessionInfoUpdate raise ProtocolDecodeError {
  let fields = protocol_require_object(value, path~)
  protocol_reject_unknown(fields, ["title", "updatedAt", "_meta"], prefix=path)
  let title = protocol_nullable_string(fields, "title", path=path + ".title")
  let updated_at = protocol_nullable_string(
    fields,
    "updatedAt",
    path=path + ".updatedAt",
  )
  let meta = protocol_meta(fields, path=path + "._meta")
  { title, updated_at, meta }
}

///|
/// Encode a session-information update as JSON.
pub fn session_info_update_to_json(
  value : SessionInfoUpdate,
) -> Json raise ProtocolDecodeError {
  let fields : Map[String, Json] = Map([])
  protocol_put_nullable(fields, "title", value.title, value => {
    Json::string(value)
  })
  protocol_put_nullable(fields, "updatedAt", value.updated_at, value => {
    Json::string(value)
  })
  protocol_put_meta(fields, value.meta)
  Json::object(fields)
}

///|
/// Decode session metadata from JSON.
pub fn session_info_from_json(
  value : Json,
  path? : String = "sessionInfo",
) -> SessionInfo raise ProtocolDecodeError {
  let fields = protocol_require_object(value, path~)
  protocol_reject_unknown(
    fields,
    ["sessionId", "cwd", "additionalDirectories", "title", "updatedAt", "_meta"],
    prefix=path,
  )
  let session_id = protocol_required_string(
    fields,
    "sessionId",
    path=path + ".sessionId",
  )
  let cwd = protocol_required_string(fields, "cwd", path=path + ".cwd")
  let cwd = protocol_validate_absolute_path(cwd, field_path=path + ".cwd")
  let additional_directories : ProtocolNullable[Array[String]] = match
    protocol_field(fields, "additionalDirectories") {
    Omitted => Omitted
    Null =>
      raise InvalidField(
        path=path + ".additionalDirectories",
        reason="additionalDirectories is not nullable",
      )
    Value(value) => {
      let values = protocol_require_array(
        value,
        path=path + ".additionalDirectories",
      )
      let directories : Array[String] = []
      for index, value in values {
        let item_path = path +
          ".additionalDirectories[" +
          index.to_string() +
          "]"
        let directory = protocol_decode_string(value, path=item_path)
        directories.push(
          protocol_validate_absolute_path(directory, field_path=item_path),
        )
      }
      Value(directories)
    }
  }
  let title = protocol_nullable_string(fields, "title", path=path + ".title")
  let updated_at = protocol_nullable_string(
    fields,
    "updatedAt",
    path=path + ".updatedAt",
  )
  let meta = protocol_meta(fields, path=path + "._meta")
  { session_id, cwd, additional_directories, title, updated_at, meta }
}

///|
/// Encode session metadata as JSON.
pub fn session_info_to_json(
  value : SessionInfo,
) -> Json raise ProtocolDecodeError {
  let fields : Map[String, Json] = Map([])
  fields["sessionId"] = Json::string(value.session_id)
  fields["cwd"] = Json::string(
    protocol_validate_absolute_path(value.cwd, field_path="cwd"),
  )
  match value.additional_directories {
    Omitted => ()
    Null =>
      raise InvalidField(
        path="additionalDirectories",
        reason="additionalDirectories is not nullable",
      )
    Value(values) => {
      let encoded : Array[Json] = []
      for value in values {
        encoded.push(
          Json::string(
            protocol_validate_absolute_path(
              value,
              field_path="additionalDirectories",
            ),
          ),
        )
      }
      fields["additionalDirectories"] = Json::array(encoded)
    }
  }
  protocol_put_nullable(fields, "title", value.title, value => {
    Json::string(value)
  })
  protocol_put_nullable(fields, "updatedAt", value.updated_at, value => {
    Json::string(value)
  })
  protocol_put_meta(fields, value.meta)
  Json::object(fields)
}

///|
fn add_session_update_discriminator(
  value : Json,
  kind : String,
) -> Json raise ProtocolDecodeError {
  match value {
    Object(fields) => {
      fields["sessionUpdate"] = Json::string(kind)
      Json::object(fields)
    }
    _ =>
      raise InvalidField(
        path="sessionUpdate",
        reason="encoded session update must be an object",
      )
  }
}

///|
fn without_session_update(fields : Map[String, Json]) -> Map[String, Json] {
  let result : Map[String, Json] = Map([])
  for key, value in fields {
    if key != "sessionUpdate" {
      result[key] = value
    }
  }
  result
}

///|
fn payload_without_session_update(fields : Map[String, Json]) -> Json {
  Json::object(without_session_update(fields))
}

///|
/// Encode one stable v1 session update variant.
pub fn session_update_to_json(
  value : SessionUpdate,
) -> Json raise ProtocolDecodeError {
  match value {
    UserMessageChunk(value) =>
      add_session_update_discriminator(
        content_chunk_to_json(value),
        "user_message_chunk",
      )
    AgentMessageChunk(value) =>
      add_session_update_discriminator(
        content_chunk_to_json(value),
        "agent_message_chunk",
      )
    AgentThoughtChunk(value) =>
      add_session_update_discriminator(
        content_chunk_to_json(value),
        "agent_thought_chunk",
      )
    ToolCall(value) =>
      add_session_update_discriminator(tool_call_to_json(value), "tool_call")
    ToolCallUpdate(value) =>
      add_session_update_discriminator(
        tool_call_update_to_json(value),
        "tool_call_update",
      )
    Plan(value) => add_session_update_discriminator(plan_to_json(value), "plan")
    AvailableCommandsUpdate(value) =>
      add_session_update_discriminator(
        available_commands_update_to_json(value),
        "available_commands_update",
      )
    CurrentModeUpdate(value) =>
      add_session_update_discriminator(
        current_mode_update_to_json(value),
        "current_mode_update",
      )
    ConfigOptionUpdate(value) =>
      add_session_update_discriminator(
        config_option_update_to_json(value),
        "config_option_update",
      )
    SessionInfoUpdate(value) =>
      add_session_update_discriminator(
        session_info_update_to_json(value),
        "session_info_update",
      )
    UsageUpdate(value) =>
      add_session_update_discriminator(
        usage_update_to_json(value),
        "usage_update",
      )
  }
}

///|
/// Decode one stable v1 session update variant.
pub fn session_update_from_json(
  value : Json,
  path? : String = "sessionUpdate",
) -> SessionUpdate raise ProtocolDecodeError {
  let fields = protocol_require_object(value, path~)
  let kind = protocol_required_string(
    fields,
    "sessionUpdate",
    path=path + ".sessionUpdate",
  )
  let payload = payload_without_session_update(fields)
  match kind {
    "user_message_chunk" =>
      UserMessageChunk(content_chunk_from_json(payload, path~))
    "agent_message_chunk" =>
      AgentMessageChunk(content_chunk_from_json(payload, path~))
    "agent_thought_chunk" =>
      AgentThoughtChunk(content_chunk_from_json(payload, path~))
    "tool_call" => ToolCall(tool_call_from_json(payload, path~))
    "tool_call_update" =>
      ToolCallUpdate(tool_call_update_from_json(payload, path~))
    "plan" => Plan(plan_from_json(payload, path~))
    "available_commands_update" =>
      AvailableCommandsUpdate(
        available_commands_update_from_json(payload, path~),
      )
    "current_mode_update" =>
      CurrentModeUpdate(current_mode_update_from_json(payload, path~))
    "config_option_update" =>
      ConfigOptionUpdate(config_option_update_from_json(payload, path~))
    "session_info_update" =>
      SessionInfoUpdate(session_info_update_from_json(payload, path~))
    "usage_update" => UsageUpdate(usage_update_from_json(payload, path~))
    _ => raise InvalidDiscriminator(path=path + ".sessionUpdate", value=kind)
  }
}

///|
/// Decode a plan entry from JSON.
pub fn plan_entry_from_json(
  value : Json,
  path? : String = "planEntry",
) -> PlanEntry raise ProtocolDecodeError {
  decode_plan_entry(value, path~)
}

///|
/// Encode a plan entry as JSON.
pub fn plan_entry_to_json(value : PlanEntry) -> Json raise ProtocolDecodeError {
  encode_plan_entry(value)
}

///|
/// Decode cost information from JSON.
pub fn cost_from_json(
  value : Json,
  path? : String = "cost",
) -> Cost raise ProtocolDecodeError {
  decode_cost(value, path~)
}

///|
/// Encode cost information as JSON.
pub fn cost_to_json(value : Cost) -> Json raise ProtocolDecodeError {
  encode_cost(value)
}

///|
/// Decode unstructured command input from JSON.
pub fn unstructured_command_input_from_json(
  value : Json,
  path? : String = "input",
) -> UnstructuredCommandInput raise ProtocolDecodeError {
  decode_unstructured_input(value, path~)
}

///|
/// Encode unstructured command input as JSON.
pub fn unstructured_command_input_to_json(
  value : UnstructuredCommandInput,
) -> Json raise ProtocolDecodeError {
  encode_unstructured_input(value)
}

///|
/// Decode a command input from JSON.
pub fn available_command_input_from_json(
  value : Json,
  path? : String = "input",
) -> AvailableCommandInput raise ProtocolDecodeError {
  decode_command_input(value, path~)
}

///|
/// Encode a command input as JSON.
pub fn available_command_input_to_json(
  value : AvailableCommandInput,
) -> Json raise ProtocolDecodeError {
  encode_command_input(value)
}

///|
/// Decode a session configuration category from JSON.
pub fn session_config_option_category_from_json(
  value : Json,
  path? : String = "category",
) -> SessionConfigOptionCategory raise ProtocolDecodeError {
  decode_config_category(value, path~)
}

///|
/// Encode a session configuration category as JSON.
pub fn session_config_option_category_to_json(
  value : SessionConfigOptionCategory,
) -> Json {
  encode_config_category(value)
}

///|
/// Decode a selectable configuration option from JSON.
pub fn session_config_select_option_from_json(
  value : Json,
  path? : String = "option",
) -> SessionConfigSelectOption raise ProtocolDecodeError {
  decode_select_option(value, path~)
}

///|
/// Encode a selectable configuration option as JSON.
pub fn session_config_select_option_to_json(
  value : SessionConfigSelectOption,
) -> Json raise ProtocolDecodeError {
  encode_select_option(value)
}

///|
/// Decode a selectable configuration group from JSON.
pub fn session_config_select_group_from_json(
  value : Json,
  path? : String = "group",
) -> SessionConfigSelectGroup raise ProtocolDecodeError {
  decode_select_group(value, path~)
}

///|
/// Encode a selectable configuration group as JSON.
pub fn session_config_select_group_to_json(
  value : SessionConfigSelectGroup,
) -> Json raise ProtocolDecodeError {
  encode_select_group(value)
}

///|
/// Decode selectable configuration values from JSON.
pub fn session_config_select_options_from_json(
  value : Json,
  path? : String = "options",
) -> SessionConfigSelectOptions raise ProtocolDecodeError {
  decode_select_options(value, path~)
}

///|
/// Encode selectable configuration values as JSON.
pub fn session_config_select_options_to_json(
  value : SessionConfigSelectOptions,
) -> Json raise ProtocolDecodeError {
  encode_select_options(value)
}

///|
/// Decode one complete configuration option from JSON.
pub fn session_config_option_from_json(
  value : Json,
  path? : String = "configOption",
) -> SessionConfigOption raise ProtocolDecodeError {
  decode_config_option(value, path~)
}

///|
/// Encode one complete configuration option as JSON.
pub fn session_config_option_to_json(
  value : SessionConfigOption,
) -> Json raise ProtocolDecodeError {
  encode_config_option(value)
}

///|
/// Structured failures produced while folding typed session updates into
/// consumer state.
///
/// These are consumer-side aggregation failures, not wire decode failures:
/// malformed payloads are rejected earlier by `session_update_from_json`, so a
/// typed update that reaches the fold always decoded cleanly.
pub(all) suberror SessionUpdateFoldError {
  DuplicateToolCall(tool_call_id~ : String)
  UnknownToolCall(tool_call_id~ : String)
  UnknownMode(mode_id~ : String)
  InvalidTimestamp(field~ : String, value~ : String)
} derive(Eq, Debug)

///|
/// One message aggregated from streamed content chunks.
///
/// Chunks that carry the same `messageId` value fold into one message, in
/// arrival order.  The v1 schema pins the aggregation contract: all chunks
/// belonging to the same message share one `messageId`, and a change of
/// `messageId` starts a new message.  A chunk without a `messageId` value
/// (omitted or JSON null) cannot be correlated, so the fold records it as its
/// own standalone message instead of merging or overwriting anything.
pub(all) struct SessionUpdateMessage {
  message_id : ProtocolNullable[MessageId]
  blocks : Array[ContentBlock]
} derive(Eq, Debug)

///|
/// Pure per-session consumer state for a stream of typed session updates.
///
/// This is the consumer-side half of the `session/update` semantics (matrix
/// rows E01-E11): the codec in this file owns wire shape and tri-state
/// fidelity, while this fold owns aggregation and replacement semantics.  The
/// fold follows the same discipline as `connection/reducer.mbt`: it is a pure
/// function of `(state, update)`, allocates the next state without mutating
/// the input state, keeps no mutable registry, and holds no global state.
/// Each session owns one fold value, so a rejected update for one session can
/// never pollute another session's state.
///
/// Semantics per variant:
///
/// - `user_message_chunk` / `agent_message_chunk` / `agent_thought_chunk`
///   (E01-E03): chunks aggregate by `messageId` within their own kind bucket;
///   the three kinds never merge with each other.
/// - `tool_call` (E04): the first occurrence creates the tracked call keyed by
///   `toolCallId`; a duplicate id is a typed error, never an overwrite.
/// - `tool_call_update` (E05): present fields replace, omitted fields keep,
///   and JSON null clears the field back to the omitted state; an unknown
///   `toolCallId` is a typed error and never fabricates a new call.
/// - `plan` (E06), `available_commands_update` (E07), and
///   `config_option_update` (E09): each update is the complete replacement
///   set; the fold never append-merges entries or options.
/// - `current_mode_update` (E08): `currentModeId` is validated against the
///   caller-supplied `known_modes`.  Consistent with the C10 boundary, the
///   codec validates only shape; which modes exist is session knowledge the
///   consumer owns.  The fold is strict: an empty known-mode set rejects every
///   mode update rather than accepting a silent default mode.
/// - `session_info_update` (E10): `title`/`updatedAt` keep tri-state
///   semantics (omitted keeps, null clears, value sets) and an `updatedAt`
///   value must parse as an RFC 3339 / ISO 8601 extended timestamp.
/// - `usage_update` (E11): replaces the previously recorded usage; the
///     non-negative `used`/`size` invariant is enforced at the codec by the
///     unsigned integer decode.
pub(all) struct SessionUpdateFold {
  known_modes : Array[SessionModeId]
  user_messages : Array[SessionUpdateMessage]
  agent_messages : Array[SessionUpdateMessage]
  agent_thoughts : Array[SessionUpdateMessage]
  tool_calls : Array[ToolCall]
  plan : Plan?
  available_commands : Array[AvailableCommand]
  current_mode_id : SessionModeId?
  config_options : Array[SessionConfigOption]
  session_title : ProtocolNullable[String]
  session_updated_at : ProtocolNullable[String]
  usage : UsageUpdate?
} derive(Eq, Debug)

///|
/// Create the initial consumer state for one session.
///
/// `known_modes` is the immutable mode set the consumer knows for the session
/// (typically `SessionModeState.available_modes` from `session/new`); it is
/// only read by `current_mode_update` validation and is never modified by any
/// applied update.
pub fn session_update_fold(
  known_modes~ : Array[SessionModeId],
) -> SessionUpdateFold {
  {
    known_modes: known_modes.copy(),
    user_messages: [],
    agent_messages: [],
    agent_thoughts: [],
    tool_calls: [],
    plan: None,
    available_commands: [],
    current_mode_id: None,
    config_options: [],
    session_title: Omitted,
    session_updated_at: Omitted,
    usage: None,
  }
}

///|
/// Apply strict `tool_call_update` patch semantics to one nullable field: a
/// present value replaces the previous value, JSON null clears the field back
/// to the omitted state, and an omitted field keeps the previous value.
fn[T] session_update_patch_field(
  current : ProtocolNullable[T],
  patch : ProtocolNullable[T],
) -> ProtocolNullable[T] {
  match patch {
    Omitted => current
    Null => Omitted
    Value(_) => patch
  }
}

///|
/// Patch semantics for nullable collection fields.  Identical to
/// `session_update_patch_field` except that a replaced collection is copied,
/// so the fold never aliases an array the caller could later mutate.
fn[T] session_update_patch_collection(
  current : ProtocolNullable[Array[T]],
  patch : ProtocolNullable[Array[T]],
) -> ProtocolNullable[Array[T]] {
  match patch {
    Omitted => current
    Null => Omitted
    Value(values) => Value(values.copy())
  }
}

///|
/// Fold one resolved tool call update into its tracked call.
///
/// A null `title` patch keeps the creation title: the v1 `ToolCall` record
/// requires a title, the consumer state cannot represent a title-less call,
/// and the fold never fabricates an empty title to simulate a clear.
fn session_update_patch_tool_call(
  call : ToolCall,
  patch : ToolCallUpdate,
) -> ToolCall {
  {
    tool_call_id: call.tool_call_id,
    title: match patch.title {
      Value(title) => title
      _ => call.title
    },
    kind: session_update_patch_field(call.kind, patch.kind),
    status: session_update_patch_field(call.status, patch.status),
    content: session_update_patch_collection(call.content, patch.content),
    locations: session_update_patch_collection(call.locations, patch.locations),
    raw_input: session_update_patch_field(call.raw_input, patch.raw_input),
    raw_output: session_update_patch_field(call.raw_output, patch.raw_output),
    meta: session_update_patch_field(call.meta, patch.meta),
  }
}

///|
/// Locate a tracked message by its `messageId` value.
fn session_update_message_index(
  messages : Array[SessionUpdateMessage],
  message_id : MessageId,
) -> Int? {
  for index, message in messages {
    match message.message_id {
      Value(id) => if id == message_id { return Some(index) }
      _ => ()
    }
  }
  None
}

///|
/// Fold one streamed chunk into a message bucket (E01-E03).
///
/// A chunk whose `messageId` carries a value appends to the message with that
/// id, creating it on first occurrence; a change of id therefore starts a new
/// message while earlier messages keep their blocks.  A chunk without a
/// `messageId` value becomes its own standalone message because nothing on
/// the wire correlates it with an existing one.
fn session_update_append_chunk(
  messages : Array[SessionUpdateMessage],
  chunk : ContentChunk,
) -> Array[SessionUpdateMessage] {
  match chunk.message_id {
    Value(id) => {
      let next = messages.copy()
      match session_update_message_index(messages, id) {
        Some(index) => {
          let message = next[index]
          let blocks = message.blocks.copy()
          blocks.push(chunk.content)
          next[index] = { message_id: message.message_id, blocks }
        }
        None =>
          next.push({ message_id: chunk.message_id, blocks: [chunk.content] })
      }
      next
    }
    _ => {
      let next = messages.copy()
      next.push({ message_id: chunk.message_id, blocks: [chunk.content] })
      next
    }
  }
}

///|
/// Locate a tracked tool call by its `toolCallId`.
fn session_update_tool_call_index(
  calls : Array[ToolCall],
  tool_call_id : ToolCallId,
) -> Int? {
  for index, call in calls {
    if call.tool_call_id == tool_call_id {
      return Some(index)
    }
  }
  None
}

///|
/// Report whether the caller-supplied known-mode set contains `mode_id`.
fn session_update_mode_known(
  known_modes : Array[SessionModeId],
  mode_id : SessionModeId,
) -> Bool {
  let mut known = false
  for mode in known_modes {
    if mode == mode_id {
      known = true
      break
    }
  }
  known
}

///|
/// Read one ASCII digit at `index`, or `None` when out of range or not a
/// digit.
fn session_update_timestamp_digit(chars : Array[Char], index : Int) -> Int? {
  if index >= chars.length() {
    None
  } else if protocol_is_digit(chars[index]) {
    Some(chars[index].to_int() - '0')
  } else {
    None
  }
}

///|
/// Read a two-digit zero-padded number at `index`.
fn session_update_timestamp_pair(chars : Array[Char], index : Int) -> Int? {
  match
    (
      session_update_timestamp_digit(chars, index),
      session_update_timestamp_digit(chars, index + 1),
    ) {
    (Some(high), Some(low)) => Some(high * 10 + low)
    _ => None
  }
}

///|
/// Read the four-digit year at the start of a timestamp.
fn session_update_timestamp_year(chars : Array[Char]) -> Int? {
  match
    (
      session_update_timestamp_digit(chars, 0),
      session_update_timestamp_digit(chars, 1),
      session_update_timestamp_digit(chars, 2),
      session_update_timestamp_digit(chars, 3),
    ) {
    (Some(a), Some(b), Some(c), Some(d)) =>
      Some(a * 1000 + b * 100 + c * 10 + d)
    _ => None
  }
}

///|
/// Proleptic Gregorian leap-year test for day-of-month validation.
fn session_update_is_leap_year(year : Int) -> Bool {
  (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}

///|
/// Days in one month; an out-of-range month yields 0 so the caller's day
/// check fails.
fn session_update_days_in_month(year : Int, month : Int) -> Int {
  match month {
    1 | 3 | 5 | 7 | 8 | 10 | 12 => 31
    4 | 6 | 9 | 11 => 30
    2 => if session_update_is_leap_year(year) { 29 } else { 28 }
    _ => 0
  }
}

///|
/// Validate the timezone designator starting at `start`: `Z`/`z`, or a
/// numeric offset `±HH:MM` within range.  The timestamp must end there.
fn session_update_timestamp_offset_valid(
  chars : Array[Char],
  start : Int,
) -> Bool {
  let length = chars.length()
  if start >= length {
    return false
  }
  match chars[start] {
    'Z' | 'z' => start + 1 == length
    '+' | '-' =>
      match
        (
          session_update_timestamp_pair(chars, start + 1),
          session_update_timestamp_pair(chars, start + 4),
        ) {
        (Some(hour), Some(minute)) =>
          if start + 3 < length && chars[start + 3] == ':' {
            hour <= 23 && minute <= 59 && start + 6 == length
          } else {
            false
          }
        _ => false
      }
    _ => false
  }
}

///|
/// Strict shape check for the RFC 3339 / ISO 8601 extended calendar form the
/// v1 schema requires for `updatedAt`:
/// `YYYY-MM-DDTHH:MM:SS[.fraction](Z|±HH:MM)` with `T`/`t` and `Z`/`z`
/// accepted, zero-padded two-digit fields, calendar-valid month and day
/// (leap-year aware), and seconds up to 60 for leap seconds.  This is a
/// shape/parse gate only; it does not normalize or rewrite the value.
fn session_update_is_valid_timestamp(value : String) -> Bool {
  let chars = value.to_array()
  let length = chars.length()
  if length < 20 {
    return false
  }
  if chars[4] != '-' || chars[7] != '-' {
    return false
  }
  if chars[10] != 'T' && chars[10] != 't' {
    return false
  }
  if chars[13] != ':' || chars[16] != ':' {
    return false
  }
  let date_valid = match
    (
      session_update_timestamp_year(chars),
      session_update_timestamp_pair(chars, 5),
      session_update_timestamp_pair(chars, 8),
      session_update_timestamp_pair(chars, 11),
      session_update_timestamp_pair(chars, 14),
      session_update_timestamp_pair(chars, 17),
    ) {
    (Some(year), Some(month), Some(day), Some(hour), Some(minute), Some(second)) =>
      month >= 1 &&
      month <= 12 &&
      day >= 1 &&
      day <= session_update_days_in_month(year, month) &&
      hour <= 23 &&
      minute <= 59 &&
      second <= 60
    _ => false
  }
  if !date_valid {
    return false
  }
  let mut offset_start = 19
  if offset_start < length && chars[offset_start] == '.' {
    offset_start += 1
    let fraction_start = offset_start
    while offset_start < length && protocol_is_digit(chars[offset_start]) {
      offset_start += 1
    }
    if offset_start == fraction_start {
      return false
    }
  }
  session_update_timestamp_offset_valid(chars, offset_start)
}

///|
/// Apply one typed session update to consumer state, returning either the
/// next state or a typed rejection.  The input state is never mutated, so a
/// caller that keeps the previous value still observes it unchanged after a
/// rejection.
pub fn session_update_apply(
  state : SessionUpdateFold,
  update : SessionUpdate,
) -> Result[SessionUpdateFold, SessionUpdateFoldError] {
  match update {
    UserMessageChunk(chunk) =>
      Ok({
        ..state,
        user_messages: session_update_append_chunk(state.user_messages, chunk),
      })
    AgentMessageChunk(chunk) =>
      Ok({
        ..state,
        agent_messages: session_update_append_chunk(state.agent_messages, chunk),
      })
    AgentThoughtChunk(chunk) =>
      Ok({
        ..state,
        agent_thoughts: session_update_append_chunk(state.agent_thoughts, chunk),
      })
    ToolCall(call) =>
      match
        session_update_tool_call_index(state.tool_calls, call.tool_call_id) {
        Some(_) => Err(DuplicateToolCall(tool_call_id=call.tool_call_id))
        None => {
          let calls = state.tool_calls.copy()
          calls.push(call)
          Ok({ ..state, tool_calls: calls })
        }
      }
    ToolCallUpdate(patch) =>
      match
        session_update_tool_call_index(state.tool_calls, patch.tool_call_id) {
        Some(index) => {
          let calls = state.tool_calls.copy()
          calls[index] = session_update_patch_tool_call(calls[index], patch)
          Ok({ ..state, tool_calls: calls })
        }
        None => Err(UnknownToolCall(tool_call_id=patch.tool_call_id))
      }
    Plan(value) => Ok({ ..state, plan: Some(value) })
    AvailableCommandsUpdate(value) =>
      Ok({ ..state, available_commands: value.available_commands.copy() })
    CurrentModeUpdate(value) =>
      if session_update_mode_known(state.known_modes, value.current_mode_id) {
        Ok({ ..state, current_mode_id: Some(value.current_mode_id) })
      } else {
        Err(UnknownMode(mode_id=value.current_mode_id))
      }
    ConfigOptionUpdate(value) =>
      Ok({ ..state, config_options: value.config_options.copy() })
    SessionInfoUpdate(value) => {
      let title = match value.title {
        Omitted => state.session_title
        updated => updated
      }
      let updated_at = match value.updated_at {
        Omitted => state.session_updated_at
        Null => Null
        Value(timestamp) =>
          if session_update_is_valid_timestamp(timestamp) {
            Value(timestamp)
          } else {
            return Err(InvalidTimestamp(field="updatedAt", value=timestamp))
          }
      }
      Ok({ ..state, session_title: title, session_updated_at: updated_at })
    }
    UsageUpdate(value) => Ok({ ..state, usage: Some(value) })
  }
}