Bounded OTP Server Integration

Copy Markdown View Source

TypeSafeSDK 0.4.0 adds TypeSafeSDK.OTP.Server, an opt-in GenServer facade for applications that want semantic evaluations to behave like asynchronous OTP work. It is inspired by the peer-process design in dannote/jev, but keeps TypeSafeSDK's existing response structs, Pristine runtime, cancellation contracts, and explicit bounds.

The base SDK still starts no application process. Your application owns the Task.Supervisor and chooses the per-server max_in_flight bound.

defmodule MyApp.Triage do
  use TypeSafeSDK.OTP.Server

  @impl true
  def init(_), do: {:ok, %{handled: 0}}

  @impl true
  def handle_call({:route, ticket}, from, state) do
    questions = [
      team: TypeSafeSDK.choice("Which team?", billing: nil, support: nil)
    ]

    {:evaluate, {from, ticket, questions}, state}
  end

  @impl true
  def handle_evaluation({:ok, response}, from, state) do
    GenServer.reply(from, {:ok, TypeSafeSDK.Response.values(response)})
    {:noreply, %{state | handled: state.handled + 1}}
  end

  def handle_evaluation({:error, error}, from, state) do
    GenServer.reply(from, {:error, error})
    {:noreply, state}
  end
end

Put the task supervisor before the semantic server in the application tree:

children = [
  {Task.Supervisor, name: MyApp.TypeSafeTasks, max_children: 64},
  {MyApp.Triage,
   client: typesafe_client,
   task_supervisor: MyApp.TypeSafeTasks,
   max_in_flight: 32,
   init_arg: nil,
   name: MyApp.Triage}
]

Callback contract

Ordinary GenServer returns keep their usual meaning. To launch semantic work, return one of these from handle_call/3, handle_cast/2, handle_info/2, handle_continue/2, or handle_evaluation/3:

{:evaluate, {tag, semantic_state, questions}, new_state}
{:evaluate, {tag, semantic_state, questions, request_options}, new_state}

tag is opaque and is returned unchanged to handle_evaluation/3. It is never automatically added to telemetry. Passing the from argument from handle_call/3 lets the eventual callback answer with GenServer.reply/2. Returning another {:evaluate, ...} from handle_evaluation/3 creates a bounded recursive decision workflow without blocking the server.

Server-level evaluation_options: are merged with request-specific options, with the request-specific value winning. Those options go through the same option-validation path as direct TypeSafeSDK.evaluate/4 calls.

Bounds and lifecycle

max_in_flight defaults to 32 and accepts 1..1024. Once the bound is reached, new semantic work is returned to handle_evaluation/3 as a normalized %TypeSafeSDK.Error{type: :runtime_capability} instead of creating another task. This is a per-server bound, not a claim about the shared HTTP transport queue.

Every request is executed with a private Pristine.Cancellation token owned by the wrapper. If the caller supplies cancellation:, a temporary Pristine watcher mirrors caller cancellation into that private token; the caller's token itself is never mutated by the server. The wrapper traps exits so orderly supervisor shutdown runs its cleanup path, stops the watcher, cancels the private token, and then stops the corresponding local task. This lets transport cleanup use cancellation without stealing ownership of an application token. As elsewhere in the SDK, local cancellation does not prove the remote service never accepted or processed a request.

A server restart rebuilds its in-memory pending map. Do not use an in-memory OTP wrapper as durable workflow state: persist the position externally when a multi-step workflow must resume after process restart.

format_status/1 exposes only the inner application state plus in_flight and max_in_flight; client credentials, pending tags, cancellation tokens, and task records are not shown.

Runnable live integration

Run mix run examples/live_otp_server.exs for a real-API integration with a caller-owned Task.Supervisor, explicit max_in_flight, simultaneous callers, opaque correlation, server/per-request option merging, typed results, private-token cleanup, and a bounded recursive handle_evaluation/3 workflow. Deterministic saturation/failure assertions remain in test/typesafe_sdk/otp_server_v040_test.exs; the live service is never required to stay slow enough to force overload.