Packages
A Fair Source multi-agent runtime with deterministic agent scoring and replayable run history.
Current section
Files
Jump to
Current section
Files
lib/syntropy/task_scheduler/selection.ex
defmodule Syntropy.TaskScheduler.Selection do
@moduledoc """
Agent selection and routing for task runs.
Resolves the requested strategy (`:single`, `:parallel`, `:all`, `:auto`, or
an explicit agent list) against the cluster-wide ranked candidate pool and
produces the selection trace that runs carry for observability.
"""
alias Syntropy.{ClusterInventory, TaskRouter}
@type selection :: %{
mode: Syntropy.TaskScheduler.resolved_mode(),
ranked_candidates: [map()],
selected_agents: [map()]
}
@doc """
Selects agents for a run.
When `agent_ids` is a list the selection is explicit; otherwise the
requested strategy is resolved against candidates ranked by
`Syntropy.TaskRouter`, excluding agents in `busy_agent_ids`.
"""
@spec select_agents(
String.t(),
[String.t()] | nil,
Syntropy.TaskScheduler.requested_strategy(),
pos_integer(),
MapSet.t() | [String.t()]
) :: selection()
def select_agents(prompt, agent_ids, _strategy, _agent_count, busy_agent_ids)
when is_list(agent_ids) do
ranked_candidates = ranked_candidates(prompt, busy_agent_ids)
available_agents = Enum.map(ranked_candidates, fn candidate -> candidate.agent end)
selected_agents =
agent_ids
|> Enum.map(&find_agent(&1, available_agents))
|> Enum.reject(&is_nil/1)
%{mode: :explicit, ranked_candidates: ranked_candidates, selected_agents: selected_agents}
end
def select_agents(prompt, nil, strategy, agent_count, busy_agent_ids) do
ranked = ranked_candidates(prompt, busy_agent_ids)
case strategy do
:single ->
%{
mode: :single,
ranked_candidates: ranked,
selected_agents: Enum.map(Enum.take(ranked, 1), & &1.agent)
}
:all ->
%{mode: :all, ranked_candidates: ranked, selected_agents: Enum.map(ranked, & &1.agent)}
:auto ->
{mode, selected_agents} = resolve_auto(ranked)
%{mode: mode, ranked_candidates: ranked, selected_agents: selected_agents}
_other ->
%{
mode: :parallel,
ranked_candidates: ranked,
selected_agents:
ranked
|> Enum.take(max(agent_count, 1))
|> Enum.map(& &1.agent)
}
end
end
@doc """
Builds the per-candidate selection trace, marking which ranked candidates
were selected for the run.
"""
@spec build_selection_trace([map()], [String.t()]) ::
[Syntropy.TaskScheduler.selection_trace_entry()]
def build_selection_trace(ranked_candidates, selected_agent_ids) do
selected_id_set = MapSet.new(selected_agent_ids)
ranked_candidates
|> Enum.with_index(1)
|> Enum.map(fn {candidate, rank} ->
%{
agent_id: candidate.agent.id,
runtime_id: candidate.agent.runtime_id,
node_id: candidate.agent.node_id,
agent_name: candidate.agent.name,
rank: rank,
selected: MapSet.member?(selected_id_set, candidate.agent.runtime_id),
relevance: candidate.relevance,
position: candidate.position,
capacity: candidate.capacity,
composite: candidate.score
}
end)
end
defp resolve_auto([]), do: {:single, []}
defp resolve_auto([top]), do: {:single, [top.agent]}
defp resolve_auto([top, second | _rest] = ranked_candidates) do
topology = Syntropy.Contract.runtime_topology_contract()
auto_strategy = topology["auto_strategy"]
relevance_margin = top.relevance - second.relevance
if top.relevance >= auto_strategy["single_relevance_threshold"] and
relevance_margin >= auto_strategy["single_margin_threshold"] do
{:single, [top.agent]}
else
parallel_count = min(auto_strategy["parallel_agent_count"], length(ranked_candidates))
{:parallel, ranked_candidates |> Enum.take(parallel_count) |> Enum.map(& &1.agent)}
end
end
defp ranked_candidates(prompt, busy_agent_ids) do
TaskRouter.rank_agents(prompt, ClusterInventory.cluster_agents(), busy_agent_ids)
end
defp find_agent(agent_id, agents) do
Enum.find(agents, fn agent ->
agent.id == agent_id or agent.runtime_id == agent_id
end)
end
end