Packages
fnord
0.8.39
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
lib/services/name_pool.ex
defmodule Services.NamePool do
@moduledoc """
A service that manages a pool of AI agent names, batch-allocating them from
the nomenclater for efficiency. Names can be checked out and optionally
checked back in for reuse within the same session.
Each checked-out name is now associated with the caller's `pid`, and you can
retrieve it via `get_name_by_pid/1`.
The pool allocates names in chunks sized to the configured workers setting to
maximize API efficiency without overwhelming the connection pool.
"""
use GenServer
@name_chunk_timeout_ms Application.compile_env(:fnord, :name_chunk_timeout_ms, 30_000)
@name __MODULE__
@type t :: %__MODULE__{
available: [String.t()],
checked_out: MapSet.t(String.t()),
all_used: MapSet.t(String.t()),
chunk_size: pos_integer(),
pid_to_name: %{optional(pid()) => String.t()},
name_to_pid: %{optional(String.t()) => pid()}
}
defstruct available: [],
checked_out: MapSet.new(),
all_used: MapSet.new(),
chunk_size: 12,
pid_to_name: %{},
name_to_pid: %{}
# -----------------------------------------------------------------------------
# Public API
# -----------------------------------------------------------------------------
@default_name "Fnord Prefect"
def default_name, do: @default_name
@doc "Starts the name pool service"
def start_link(opts \\ []) do
name = Keyword.get(opts, :name, @name)
init_opts = Keyword.drop(opts, [:name])
GenServer.start_link(__MODULE__, init_opts, name: name)
end
@doc """
Checks out a name from the pool. If the pool is empty or running low,
automatically allocates a new chunk of names.
Returns `{:ok, name}` or `{:error, reason}`.
"""
@spec checkout_name(atom() | pid()) :: {:ok, String.t()} | {:error, String.t()}
def checkout_name(server \\ @name) do
GenServer.call(server, :checkout_name, 30_000)
end
@doc """
Checks a name back into the pool for potential reuse. This is optional -
names that are never checked back in will simply be lost when the session ends.
"""
@spec checkin_name(String.t(), atom() | pid()) :: :ok
def checkin_name(name, server \\ @name) when is_binary(name) do
GenServer.cast(server, {:checkin_name, name})
end
@doc "Returns pool statistics for debugging/monitoring"
def pool_stats(server \\ @name) do
GenServer.call(server, :pool_stats)
end
@doc "Resets the pool state (mainly for testing)"
def reset(server \\ @name) do
GenServer.call(server, :reset)
end
@doc """
Returns `{:ok, name}` if the given `pid` has a checked‐out name,
or `{:error, :not_found}` otherwise.
"""
@spec get_name_by_pid(pid(), atom() | pid()) :: {:ok, String.t()} | {:error, :not_found}
def get_name_by_pid(pid, server \\ @name) when is_pid(pid) do
GenServer.call(server, {:get_name_by_pid, pid})
end
@doc """
Restores the association between a `pid` and a `name`. This is useful to
re-associate a name after a process restart or similar event.
"""
def restore_name(pid, name, server \\ @name) do
GenServer.cast(server, {:restore_name, pid, name})
end
# -----------------------------------------------------------------------------
# GenServer Callbacks
# -----------------------------------------------------------------------------
@impl GenServer
def init(_opts) do
chunk_size =
Application.get_env(:fnord, :workers, 12)
state = %__MODULE__{
available: [],
checked_out: MapSet.new(),
all_used: MapSet.new(),
chunk_size: chunk_size,
pid_to_name: %{},
name_to_pid: %{}
}
{:ok, state}
end
@impl GenServer
def handle_call(:checkout_name, {caller_pid, _ref}, state) do
if Map.has_key?(state.pid_to_name, caller_pid) do
{:reply, {:ok, Map.get(state.pid_to_name, caller_pid)}, state}
else
case ensure_names_available(state) do
{:ok, updated_state} ->
case updated_state.available do
[name | remaining] ->
new_state = %{
updated_state
| available: remaining,
checked_out: MapSet.put(updated_state.checked_out, name),
pid_to_name: Map.put(updated_state.pid_to_name, caller_pid, name),
name_to_pid: Map.put(updated_state.name_to_pid, name, caller_pid)
}
{:reply, {:ok, name}, new_state}
[] ->
UI.error("Unable to allocate names from pool")
{:reply, {:error, "No names available"}, updated_state}
end
{:error, reason} ->
UI.error("Failed to ensure names available", reason)
{:reply, {:error, reason}, state}
end
end
end
@impl GenServer
def handle_call(:pool_stats, _from, state) do
stats = %{
available_count: length(state.available),
checked_out_count: MapSet.size(state.checked_out),
all_used_count: MapSet.size(state.all_used),
chunk_size: state.chunk_size
}
{:reply, stats, state}
end
@impl GenServer
def handle_call(:reset, _from, state) do
new_state = %{
state
| available: [],
checked_out: MapSet.new(),
all_used: MapSet.new(),
pid_to_name: %{},
name_to_pid: %{}
}
{:reply, :ok, new_state}
end
@impl GenServer
def handle_call({:get_name_by_pid, pid}, _from, state) do
case Map.fetch(state.pid_to_name, pid) do
{:ok, name} ->
{:reply, {:ok, name}, state}
:error ->
{:reply, {:error, :not_found}, state}
end
end
@impl GenServer
def handle_cast({:checkin_name, name}, state) do
if MapSet.member?(state.checked_out, name) do
pid = Map.get(state.name_to_pid, name)
new_state = %{
state
| available: [name | state.available],
checked_out: MapSet.delete(state.checked_out, name),
pid_to_name: Map.delete(state.pid_to_name, pid),
name_to_pid: Map.delete(state.name_to_pid, name)
}
{:noreply, new_state}
else
UI.warn("Attempted to check in name that wasn't checked out", name)
{:noreply, state}
end
end
@impl GenServer
def handle_cast({:restore_name, pid, name}, state) do
prev_pid = Map.get(state.name_to_pid, name)
state =
case prev_pid do
nil ->
state
^pid ->
state
other ->
%{
state
| pid_to_name: Map.delete(state.pid_to_name, other),
name_to_pid: Map.delete(state.name_to_pid, name)
}
end
new_state = %{
state
| checked_out: MapSet.put(state.checked_out, name),
pid_to_name: Map.put(state.pid_to_name, pid, name),
name_to_pid: Map.put(state.name_to_pid, name, pid),
available: List.delete(state.available, name)
}
{:noreply, new_state}
end
@doc false
# Ensures there are names available, allocating a new chunk if the pool is empty
defp ensure_names_available(%__MODULE__{available: []} = state) do
allocate_name_chunk(state)
end
defp ensure_names_available(state) do
{:ok, state}
end
# Allocates a chunk of names from the nomenclater
defp allocate_name_chunk(state) do
Application.get_env(:fnord, :nomenclater, :real)
|> case do
:fake ->
start_num = MapSet.size(state.all_used) + 1
end_num = start_num + state.chunk_size - 1
names =
start_num..end_num
|> Enum.map(&"NPC ##{&1}")
new_state = %{
state
| available: names ++ state.available,
all_used: MapSet.union(state.all_used, MapSet.new(names))
}
{:ok, new_state}
:real ->
used_names = MapSet.to_list(state.all_used)
task =
Task.async(fn ->
AI.Agent.Nomenclater
# `named?: false` prevents circular dependency with ourselves
|> AI.Agent.new(named?: false)
|> AI.Agent.get_response(%{
want: state.chunk_size,
used: used_names
})
end)
# Use Task.yield to allow configurable timeout
case Task.yield(task, @name_chunk_timeout_ms) do
{:ok, {:ok, names}} when is_list(names) ->
new_state = %{
state
| available: names ++ state.available,
all_used: MapSet.union(state.all_used, MapSet.new(names))
}
{:ok, new_state}
{:ok, {:error, reason}} ->
UI.error("Failed to make up names for your agents", reason)
{:error, reason}
nil ->
Task.shutdown(task, :brutal_kill)
UI.error("Name allocation task timed out")
{:error, :timeout}
end
end
end
end