///|
/// Plain-text projection of a tool outcome (attachments contribute nothing
/// here; they travel the message-level strategy below).
pub fn outcome_wire_text(outcome : @posoco.ToolOutcome) -> String {
  match outcome {
    Success(content~, ..) => content
    SuccessWithAttachments(content~, ..) => content
    ToolReportedError(content~, ..) => content
    RuntimeFailure(message~, ..) => message
    NotExecuted(reason~, ..) => reason.to_string()
  }
}

///|
fn outcome_attachments(outcome : @posoco.ToolOutcome) -> Array[@posoco.Content] {
  match outcome {
    SuccessWithAttachments(attachments~, ..) => attachments
    _ => []
  }
}

///|
/// Fold tool-result attachments into the chat-completions wire shape.
///
/// The `tool` role carries only a text `content` string on this protocol, so
/// attachment blocks cannot ride the tool message itself:
/// - `supports_images=true`: a contiguous run of tool messages keeps its
///   text, and the run's attachments collapse into ONE synthetic user
///   message emitted right after the run. The fold is a pure function of the
///   message array, so provider prefix caches replay identically.
/// - `supports_images=false`: every attachment degrades to the explicit
///   `[Attached …, omitted]` placeholder appended to its tool text — never a
///   silent drop.
pub fn fold_tool_attachments(
  messages : Array[@posoco.Message],
  supports_images : Bool,
) -> Array[@posoco.Message] {
  let out : Array[@posoco.Message] = []
  let run : Array[@posoco.Message] = []
  let blocks : Array[@posoco.Content] = []
  for msg in messages {
    match msg {
      @posoco.Message::ToolMessage(outcome~, ..) => {
        run.push(msg)
        for block in outcome_attachments(outcome) {
          blocks.push(block)
        }
      }
      _ => {
        flush_tool_run(out, run, blocks, supports_images)
        out.push(msg)
      }
    }
  }
  flush_tool_run(out, run, blocks, supports_images)
  out
}

///|
/// Emit one buffered tool run: keep the messages as-is and append the
/// synthetic media message, or degrade every attachment to a placeholder.
fn flush_tool_run(
  out : Array[@posoco.Message],
  run : Array[@posoco.Message],
  blocks : Array[@posoco.Content],
  supports_images : Bool,
) -> Unit {
  if run.is_empty() {
    return
  }
  if supports_images {
    for msg in run {
      out.push(msg)
    }
    if !blocks.is_empty() {
      let content : Array[@posoco.Content] = [
        @posoco.Content::Text("[media attachments from the tool results above]"),
      ]
      for block in blocks {
        content.push(block)
      }
      out.push(@posoco.Message::UserMessage(content~))
    }
  } else {
    for msg in run {
      out.push(degrade_tool_attachments(msg))
    }
  }
  run.clear()
  blocks.clear()
}

///|
/// Append explicit omission placeholders for a tool message's attachments
/// (endpoint or model cannot accept image input).
fn degrade_tool_attachments(msg : @posoco.Message) -> @posoco.Message {
  match msg {
    @posoco.Message::ToolMessage(call_id~, tool_name~, outcome~) => {
      let attachments = outcome_attachments(outcome)
      if attachments.is_empty() {
        msg
      } else {
        let builder = StringBuilder()
        builder.write_string(outcome_wire_text(outcome))
        for block in attachments {
          match block {
            @posoco.Content::Image(media_type~, data~) =>
              builder.write_string(image_placeholder(media_type, data))
            @posoco.Content::Text(_) => ()
          }
        }
        @posoco.Message::ToolMessage(
          call_id~,
          tool_name~,
          outcome=@posoco.ToolOutcome::Success(
            content=builder.to_string(),
            structured=None,
          ),
        )
      }
    }
    _ => msg
  }
}