lib/openagents/incidents/triage.ex

58e6347eeb72 · 2 KB

defmodule OpenAgents.Incidents.Triage do
  @moduledoc """
  Typed classifier for failure reasons — never ad-hoc string matching for the
  decision itself, only exact-code lookups over a modeled table.

  - `expected`  — the user or the world caused it (cancelled, computer offline,
    an agent at capacity). State it plainly; do not escalate.
  - `degraded`  — a transient, usually-retryable fault (a provider hiccup,
    a task exit, a transport drop). Record it; escalate only on recurrence.
  - `anomalous` — a fault Sarah should not have hit, or any code this table does
    not recognize. Open an incident and notify.

  Defaulting the unknown to `anomalous` is deliberate: a failure mode we did not
  foresee is exactly the one worth surfacing.
  """

  @expected MapSet.new(~w(
    cancelled
    missing_terminal_event
    machine_offline
    machine_not_found
    ambiguous_machine
    agent_not_available
    rate_limited
    turn_in_progress
    owner_not_signed_in
    delegation_refused
    delegation_cancelled
  ))

  @degraded MapSet.new(~w(
    provider_timeout
    provider_failure
    provider_error
    turn_timeout
    machine_disconnected
    machine_timeout
    machine_revoked
    runtime_restarted
    delegation_timeout
    task_exit
    transport
    provider_task_exited
  ))

  @doc "Classify a failure code into a severity tier."
  @spec classify(String.t() | atom() | nil) :: String.t()
  def classify(code) when is_atom(code) and not is_nil(code), do: classify(Atom.to_string(code))

  def classify(code) when is_binary(code) do
    # A code may be `family:detail`; classify on the family head.
    head = code |> String.split(":", parts: 2) |> List.first()

    cond do
      MapSet.member?(@expected, head) -> "expected"
      MapSet.member?(@degraded, head) -> "degraded"
      true -> "anomalous"
    end
  end

  def classify(_code), do: "anomalous"

  @doc "True when the incident warrants opening + notifying (not merely stating)."
  @spec escalate?(String.t()) :: boolean()
  def escalate?(severity), do: severity == "anomalous"
end