Current section
Files
Jump to
Current section
Files
lib/asm/provider_registry.ex
defmodule ASM.ProviderRegistry do
@moduledoc """
Resolves providers to backend lanes and runtime metadata.
Lane selection is discovery-driven and independent from execution mode:
- `provider_info/1` describes the provider and installed runtime-kit surface
- `lane_info/2` resolves the preferred lane from `:auto | :core | :sdk`
- `resolve/2` applies execution-mode compatibility to produce the effective backend
"""
alias ASM.{Error, Provider}
@type lane :: :auto | :core | :sdk
@type execution_mode :: :local | :remote_node
@type provider_info :: %{
provider: Provider.t(),
core_profile: module(),
core_profile_id: atom(),
sdk_runtime: module() | nil,
sdk_available?: boolean(),
available_lanes: [atom()],
default_lane: :auto,
core_capabilities: [atom()],
sdk_capabilities: [atom()],
observability: map()
}
@type lane_info :: %{
provider: Provider.t(),
requested_lane: lane(),
preferred_lane: :core | :sdk,
backend: module(),
core_profile: module(),
core_profile_id: atom(),
sdk_runtime: module() | nil,
sdk_available?: boolean(),
available_lanes: [atom()],
capabilities: [atom()],
lane_reason: atom(),
observability: map()
}
@type resolution :: %{
provider: Provider.t(),
requested_lane: lane(),
preferred_lane: :core | :sdk,
lane: :core | :sdk,
backend: module(),
sdk_available?: boolean(),
core_profile: module(),
core_profile_id: atom(),
sdk_runtime: module() | nil,
capabilities: [atom()],
execution_mode: execution_mode(),
lane_reason: atom(),
lane_fallback_reason: atom() | nil,
observability: map()
}
@spec supported_providers() :: [atom()]
def supported_providers do
Provider.supported_providers()
end
@spec fetch(atom() | Provider.t()) :: {:ok, Provider.t()} | {:error, Error.t()}
def fetch(provider), do: Provider.resolve(provider)
@spec core_profile_id(atom()) :: {:ok, atom()} | {:error, Error.t()}
def core_profile_id(provider) do
with {:ok, %Provider{} = config} <- fetch(provider) do
{:ok, config.name}
end
end
@spec provider_info(atom() | Provider.t()) :: {:ok, provider_info()} | {:error, Error.t()}
def provider_info(provider) do
with {:ok, %Provider{} = config} <- fetch(provider) do
{:ok, provider_info_from_config(config)}
end
end
@spec sdk_available?(atom() | Provider.t()) :: boolean()
def sdk_available?(provider) do
with {:ok, %Provider{} = config} <- fetch(provider),
runtime when is_atom(runtime) <- config.sdk_runtime do
runtime_available?(runtime)
else
_ -> false
end
end
@spec lane_info(atom() | Provider.t(), keyword()) :: {:ok, lane_info()} | {:error, Error.t()}
def lane_info(provider, opts \\ []) do
with {:ok, %Provider{} = config} <- fetch(provider),
{:ok, requested_lane} <- requested_lane(opts) do
sdk_available? = sdk_available?(config)
provider_info = provider_info_from_config(config)
case select_preferred_lane(requested_lane, sdk_available?) do
{:ok, preferred_lane, lane_reason} ->
capabilities = lane_capabilities(config, preferred_lane)
{:ok,
%{
provider: config,
requested_lane: requested_lane,
preferred_lane: preferred_lane,
backend: backend_module(preferred_lane),
core_profile: config.core_profile,
core_profile_id: config.name,
sdk_runtime: config.sdk_runtime,
sdk_available?: sdk_available?,
available_lanes: provider_info.available_lanes,
capabilities: capabilities,
lane_reason: lane_reason,
observability:
Map.merge(provider_info.observability, %{
requested_lane: requested_lane,
preferred_lane: preferred_lane,
lane_reason: lane_reason,
capabilities: capabilities
})
}}
{:error, %Error{} = error} ->
{:error, error}
end
end
end
@spec resolve(atom(), keyword()) :: {:ok, resolution()} | {:error, Error.t()}
def resolve(provider, opts \\ []) do
with {:ok, info} <- lane_info(provider, opts),
{:ok, execution_mode} <- execution_mode(opts) do
resolve_for_execution_mode(info, execution_mode)
end
end
@spec resolve_lane(atom() | Provider.t(), keyword()) ::
{:ok, :core | :sdk} | {:error, Error.t()}
def resolve_lane(provider, opts) when is_list(opts) do
case resolve(provider, opts) do
{:ok, %{lane: lane}} -> {:ok, lane}
{:error, %Error{} = error} -> {:error, error}
end
end
defp backend_module(:core), do: ASM.ProviderBackend.Core
defp backend_module(:sdk), do: ASM.ProviderBackend.SDK
defp provider_info_from_config(%Provider{} = config) do
sdk_available? = sdk_available?(config)
%{
provider: config,
core_profile: config.core_profile,
core_profile_id: config.name,
sdk_runtime: config.sdk_runtime,
sdk_available?: sdk_available?,
available_lanes: available_lanes(sdk_available?),
default_lane: :auto,
core_capabilities: module_capabilities(config.core_profile),
sdk_capabilities: if(sdk_available?, do: module_capabilities(config.sdk_runtime), else: []),
observability: %{
provider: config.name,
provider_display_name: config.display_name,
core_profile_id: config.name,
sdk_runtime: config.sdk_runtime,
sdk_available?: sdk_available?,
available_lanes: available_lanes(sdk_available?),
default_lane: :auto
}
}
end
defp requested_lane(opts) when is_list(opts) do
case Keyword.get(opts, :lane, :auto) do
lane when lane in [:auto, :core, :sdk] ->
{:ok, lane}
other ->
{:error, config_error("invalid lane #{inspect(other)}; expected :auto, :core, or :sdk")}
end
end
defp execution_mode(opts) when is_list(opts) do
case Keyword.get(opts, :execution_mode, :local) do
mode when mode in [:local, :remote_node] ->
{:ok, mode}
other ->
{:error,
config_error("invalid execution_mode #{inspect(other)}; expected :local or :remote_node")}
end
end
defp select_preferred_lane(:auto, true), do: {:ok, :sdk, :sdk_available}
defp select_preferred_lane(:auto, false), do: {:ok, :core, :sdk_unavailable}
defp select_preferred_lane(:core, _sdk_available?), do: {:ok, :core, :explicit_core}
defp select_preferred_lane(:sdk, true), do: {:ok, :sdk, :explicit_sdk}
defp select_preferred_lane(:sdk, false) do
{:error, config_error("sdk lane requested but runtime kit is unavailable")}
end
defp resolve_for_execution_mode(
%{requested_lane: :sdk, preferred_lane: :sdk},
:remote_node
) do
{:error, config_error("sdk lane is unavailable for :remote_node execution")}
end
defp resolve_for_execution_mode(%{preferred_lane: :sdk} = info, :remote_node) do
finalize_resolution(info, :core, :remote_node, :sdk_remote_unsupported)
end
defp resolve_for_execution_mode(%{} = info, execution_mode) do
finalize_resolution(info, info.preferred_lane, execution_mode, nil)
end
defp finalize_resolution(%{} = info, lane, execution_mode, lane_fallback_reason) do
capabilities = lane_capabilities(info.provider, lane)
{:ok,
%{
provider: info.provider,
requested_lane: info.requested_lane,
preferred_lane: info.preferred_lane,
lane: lane,
backend: backend_module(lane),
sdk_available?: info.sdk_available?,
core_profile: info.core_profile,
core_profile_id: info.core_profile_id,
sdk_runtime: info.sdk_runtime,
capabilities: capabilities,
execution_mode: execution_mode,
lane_reason: info.lane_reason,
lane_fallback_reason: lane_fallback_reason,
observability:
Map.merge(info.observability, %{
lane: lane,
backend: backend_module(lane),
execution_mode: execution_mode,
capabilities: capabilities,
lane_fallback_reason: lane_fallback_reason
})
}}
end
defp available_lanes(true), do: [:core, :sdk]
defp available_lanes(false), do: [:core]
defp lane_capabilities(%Provider{} = config, :core),
do: module_capabilities(config.core_profile)
defp lane_capabilities(%Provider{} = config, :sdk), do: module_capabilities(config.sdk_runtime)
defp module_capabilities(module) when is_atom(module) do
if runtime_available?(module) and function_exported?(module, :capabilities, 0) do
module.capabilities()
else
[]
end
end
defp runtime_available?(runtime) when is_atom(runtime) do
case runtime_loader().(runtime) do
true -> true
{:module, _module} -> true
_ -> false
end
end
defp runtime_loader do
:agent_session_manager
|> Application.get_env(__MODULE__, [])
|> Keyword.get(:runtime_loader, &Code.ensure_loaded?/1)
end
defp config_error(message) do
Error.new(:config_invalid, :config, message)
end
end