// Stage 2 of the lowering: dialect-agnostic LlmContext -> provider wire
// format, one impl per provider protocol, quirks inside the impl and never
// in the IR. `raise` is a keyword, so the inverse direction is `lift`.

///|
pub fn StreamState::new() -> StreamState {
  { chunks: [] }
}

///|
/// Append a chunk, returning a new state (pure — the shell owns the loop).
pub fn StreamState::push(self : StreamState, chunk : String) -> StreamState {
  let chunks = self.chunks.copy()
  chunks.push(chunk)
  { chunks, }
}

///|
/// The accumulated raw body so far.
pub fn StreamState::text(self : StreamState) -> String {
  self.chunks.join("")
}

///|
/// Default: this dialect does not name a model, and whoever needs one falls
/// back to whatever it was configured with.
impl Dialect with fn model(_self : Self) -> String {
  ""
}

///|
/// Default: fresh accumulator.
impl Dialect with fn stream_init(_self : Self) -> StreamState {
  StreamState::new()
}

///|
/// Default: buffer the chunk, emit no incremental events.
impl Dialect with fn stream_feed(
  _self : Self,
  state : StreamState,
  chunk~ : String,
) -> (StreamState, Array[StreamEvent]) raise DialectError {
  (state.push(chunk), [])
}

///|
/// Default: parse the accumulated body as one JSON reply and lift it.
impl Dialect with fn stream_finish(self : Self, state : StreamState) -> ContextMessage raise DialectError {
  let text = state.text()
  let json = @json.parse(text) catch {
    _ => raise DialectError::MalformedReply("stream body was not valid JSON")
  }
  self.lift_message(json)
}