Jev.Server is a GenServer that talks to Jev by replying. It owns the real GenServer callbacks and delegates to your module, the way GenStage and Agent are built, so every GenServer facility keeps working.

Callbacks

Your module implements init/1, any of the usual handle_call/3, handle_cast/2, handle_info/2, handle_continue/2, and one new callback:

handle_answer(reply | {:error, reason}, tag, state)

Message, tag, state mirrors handle_call's message, from, state.

Sending to Jev

From any callback, return

{:reply, {tag, state, questions}, your_state}

and the request is posted under Jev.TaskSupervisor without blocking the server. tag is any term; it comes back with the answer. state is what the questions are about. questions is a keyword list or map in any of the shorthands.

Per-request options such as model: go in a fourth element. The questions then need their own brackets:

{:reply, {tag, state, [kind: {"Which?", %{a: nil, b: nil}}], [model: "jev-preview"]}, s}

The other return values are the GenServer ones: {:noreply, s}, {:noreply, s, timeout | :hibernate | {:continue, term}}, and {:stop, reason, s}.

Answering callers

Because the answer is not known when handle_call/3 returns, callers are answered later with GenServer.reply/2. Passing from as the tag is the common case:

def handle_call({:labels, issue}, from, s), do: {:reply, {from, issue, @questions}, s}

def handle_answer(%{kind: k}, from, s) do
  GenServer.reply(from, [k])
  {:noreply, s}
end

Set the call timeout with the API latency in mind. A budget of 30 seconds covers the default receive timeout plus retries.

Requests in flight

A server can have any number of requests outstanding. Each reply carries its tag, so a clause knows which caller, page, or step it belongs to. The pending map in the wrapper state holds the outstanding task references, and :sys.get_status/1 shows your inner state rather than the wrapper.

Errors

Every failure arrives at handle_answer/3 as {:error, reason}:

  • a non-2xx response after retries, as {:error, %Jev.Error{status: status, body: body, request_id: id}}
  • a transport failure, as {:error, %Req.TransportError{}} or another exception
  • a crashed request, for example state that cannot be encoded, as {:error, {exception, stacktrace}}

The request runs under Task.Supervisor.async_nolink/4, so the crash never reaches the server. A retry is just a clause that replies again:

def handle_answer({:error, %Jev.Error{status: 529}}, {attempt, issue} = tag, s) when attempt < 3 do
  {:reply, {{attempt + 1, issue}, issue, @questions}, s}
end

Note that Jev.HTTP already retries 429 and 529 with backoff before the error reaches you, so this is for the rare case that outlives those retries.

Routing on the reply

The reply is a plain map, so routing is pattern matching in clause order:

# A security flag wins regardless of the rest.
def handle_answer(%{security: p}, from, s) when p > 0.5, do: done(from, [:security], s)

# Confident and serious.
def handle_answer(%{kind: :bug, severity: sev, confidence: %{kind: c}}, from, s)
    when c > 0.85 and sev >= 2,
    do: done(from, [:bug, :"priority:high"], s)

# Confident enough to act.
def handle_answer(%{kind: k, confidence: %{kind: c}}, from, s) when c > 0.6, do: done(from, [k], s)

# Too close to call: hand the top two to a human.
def handle_answer(%{probabilities: %{kind: ps}}, from, s) do
  [a, b | _] = ps |> Enum.sort_by(&elem(&1, 1), :desc) |> Enum.map(&elem(&1, 0))
  done(from, [:"needs-triage", {:one_of, a, b}], s)
end

The TypeSafe confidence guide suggests acting above 0.9, confirming between 0.5 and 0.9, and escalating below 0.5, with stricter thresholds for actions that are harder to reverse. Those are three guards.

Starting a server

use Jev.Server defines a child_spec/1, so a server goes in a supervision tree like any other child:

children = [
  {Triage, []},
  {Jev.Server, [Locate, page]}   # or explicitly
]

Jev.Server.start_link/3 takes the module, the init argument, and GenServer.start_link/3 options such as name:.

The :jev application starts Jev.TaskSupervisor, which the servers post through. Nothing else needs to be started.