///|
/// ModelPort: LLM calling interface. Decides HOW to compact; posoco decides
/// WHEN. `chat` returns `ModelCallResult` with `processed_messages`.
///
/// Both model-side entry points receive an `InvocationScope` identifying the
/// session/run the call serves (`chat` additionally carries the reducer-
/// allocated effect id). Adapters that key behaviour on session identity —
/// continuity, telemetry, cost attribution, per-session policy — read it from
/// `scope`; scope-agnostic adapters ignore it. Treat it as read-only.
pub(open) trait ModelPort {
  /// One chat call. `Stream(cb)` emits chunks; `NoStream` skips chunk work.
  async fn chat(
    Self,
    scope : @kernel.InvocationScope,
    messages : Array[@kernel.Message],
    tools : Array[@kernel.ToolDef],
    options : @types.ChatOptions,
    stream : @types.StreamMode,
  ) -> @kernel.ModelCallResult raise @error.ModelError

  /// Compact the conversation. `trigger` (Auto/Manual) is advisory — the
  /// modelport MAY use a cheaper strategy for Auto. Modelports that do not
  /// support compact should raise `ModelError::ResponseParse`.
  /// `scope.effect_id` is always `None` here: compact is host-driven, not a
  /// reducer-allocated effect.
  async fn compact(
    Self,
    scope : @kernel.InvocationScope,
    messages : Array[@kernel.Message],
    options : @types.ChatOptions,
    trigger : @kernel.CompactTrigger,
  ) -> @kernel.CompactResult raise @error.ModelError

  /// Self-description: which config values this modelport accepts. A mismatch
  /// is not fatal — posoco emits a warning and resets. Pure function.
  fn provider_config(Self) -> ProviderConfig
}