///|
/// Host-runtime decorator for the composition seam: core basic tools (the
/// built-in memory surface) execute through their own providers ahead of the
/// host's runtime chain, so a host that wires `Agent::with_runtime` without
/// knowing the basic names still runs them. Everything else — model calls,
/// cancellation, compact, and every non-basic tool — forwards to the host
/// runtime untouched.

///|
priv struct BasicFirstHostRuntime {
  inner : &@kernel_exec.HostRuntime
  basic : Map[String, &@port.ToolProvider]
  /// Same registration semantics as `PortRuntime`: populated only by basic
  /// providers that call `ToolCallContext.register_cancel`; dropped at
  /// settle. Memory tools are instantaneous and never register.
  cancels : Map[Int, &@port.ToolCancellable]
}

///|
impl @kernel_exec.HostRuntime for BasicFirstHostRuntime with fn call_model(
  self,
  scope : @kernel.InvocationScope,
  messages : ArrayView[@kernel.Message],
  tool_definitions : Array[@kernel.ToolDef],
  call_options : Json,
  on_chunk : @kernel_exec.HostChunkCallback?,
) -> Result[@kernel.ModelCallResult, @kernel.ModelFailure] {
  self.inner.call_model(
    scope, messages, tool_definitions, call_options, on_chunk,
  )
}

///|
impl @kernel_exec.HostRuntime for BasicFirstHostRuntime with fn execute_tool(
  self,
  effect_id : @kernel.EffectId,
  owner_id : @kernel.OwnerId,
  call_id : @kernel.CallId,
  tool_name : @kernel.ToolName,
  arguments : Json,
) -> @kernel.ToolOutcome {
  let name = tool_name.to_string()
  match self.basic.get(name) {
    Some(provider) => {
      let key = effect_id.to_int()
      let runtime = self
      let call_ctx : @port.ToolCallContext = {
        effect_id,
        call_id,
        name: tool_name,
        arguments,
        register_cancel: fn(c : &@port.ToolCancellable) {
          runtime.cancels[key] = c
        },
      }
      let outcome = provider.execute(call_ctx) catch {
        error =>
          RuntimeFailure(
            error_category="ToolProviderError",
            message=error.to_string(),
          )
      }
      self.cancels.remove(key)
      outcome
    }
    None =>
      self.inner.execute_tool(
        effect_id, owner_id, call_id, tool_name, arguments,
      )
  }
}

///|
impl @kernel_exec.HostRuntime for BasicFirstHostRuntime with fn cancel_effects(
  self,
  effect_ids : Array[@kernel.EffectId],
  reason : @kernel.CancelReason,
) -> Array[(@kernel.EffectId, @kernel.CancelDisposition)] {
  let dispositions : Array[(@kernel.EffectId, @kernel.CancelDisposition)] = []
  let delegated : Array[@kernel.EffectId] = []
  for id in effect_ids {
    match self.cancels.get(id.to_int()) {
      Some(handler) => {
        let disposition = handler.cancel(id, reason) catch {
          _ => NotPropagated
        }
        dispositions.push((id, disposition))
      }
      None => delegated.push(id)
    }
  }
  if !delegated.is_empty() {
    for pair in self.inner.cancel_effects(delegated, reason) {
      dispositions.push(pair)
    }
  }
  dispositions
}

///|
impl @kernel_exec.HostRuntime for BasicFirstHostRuntime with fn compact(
  self,
  scope : @kernel.InvocationScope,
  messages : ArrayView[@kernel.Message],
  tools : Array[@kernel.ToolDef],
  call_options : Json,
  trigger : @kernel.CompactTrigger,
) -> Result[@kernel.CompactResult, @kernel.ModelFailure] {
  self.inner.compact(scope, messages, tools, call_options, trigger)
}