Current section
Files
Jump to
Current section
Files
src/agent_discovery.erl
%% agent_discovery.erl
%% Enhanced registry for agent discovery and capabilities
-module(agent_discovery).
-behaviour(gen_server).
%% API
-export([
start_link/1,
register_agent/3,
unregister_agent/1,
find_agent/1,
find_by_capability/1,
find_all/0,
get_agent_info/1,
update_agent_status/2,
update_agent_capabilities/2,
broadcast_heartbeat/0
]).
%% gen_server callbacks
-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3
]).
-define(SERVER, ?MODULE).
-define(DEFAULT_HEARTBEAT_INTERVAL, 30000). % 30 seconds
-record(state, {
agents = #{} :: map(), % AgentId -> {Pid, Timestamp, Capabilities, Status}
capabilities_index = #{} :: map(), % Capability -> [AgentId]
heartbeat_interval = ?DEFAULT_HEARTBEAT_INTERVAL :: non_neg_integer(),
heartbeat_ref :: reference() | undefined
}).
%% API Functions
%% Start the agent discovery service
start_link(Options) ->
gen_server:start_link({local, ?SERVER}, ?MODULE, Options, []).
%% Register an agent with capabilities
-spec register_agent(AgentId, Pid, Capabilities) -> Result when
AgentId :: term(),
Pid :: pid(),
Capabilities :: map(),
Result :: ok | {error, Reason},
Reason :: term().
register_agent(AgentId, Pid, Capabilities) ->
gen_server:call(?SERVER, {register_agent, AgentId, Pid, Capabilities}).
%% Unregister an agent
-spec unregister_agent(AgentId) -> Result when
AgentId :: term(),
Result :: ok | {error, Reason},
Reason :: term().
unregister_agent(AgentId) ->
gen_server:call(?SERVER, {unregister_agent, AgentId}).
%% Find an agent by ID
-spec find_agent(AgentId) -> Result when
AgentId :: term(),
Result :: {ok, AgentInfo} | {error, Reason},
AgentInfo :: map(),
Reason :: term().
find_agent(AgentId) ->
gen_server:call(?SERVER, {find_agent, AgentId}).
%% Find agents by capability
-spec find_by_capability(Capability) -> Result when
Capability :: term(),
Result :: {ok, [AgentInfo]} | {error, Reason},
AgentInfo :: map(),
Reason :: term().
find_by_capability(Capability) ->
gen_server:call(?SERVER, {find_by_capability, Capability}).
%% Get all registered agents
-spec find_all() -> Result when
Result :: {ok, [AgentInfo]} | {error, Reason},
AgentInfo :: map(),
Reason :: term().
find_all() ->
gen_server:call(?SERVER, find_all).
%% Get detailed information about an agent
-spec get_agent_info(AgentId) -> Result when
AgentId :: term(),
Result :: {ok, AgentInfo} | {error, Reason},
AgentInfo :: map(),
Reason :: term().
get_agent_info(AgentId) ->
gen_server:call(?SERVER, {get_agent_info, AgentId}).
%% Update agent status
-spec update_agent_status(AgentId, Status) -> Result when
AgentId :: term(),
Status :: atom(),
Result :: ok | {error, Reason},
Reason :: term().
update_agent_status(AgentId, Status) ->
gen_server:call(?SERVER, {update_agent_status, AgentId, Status}).
%% Update agent capabilities
-spec update_agent_capabilities(AgentId, Capabilities) -> Result when
AgentId :: term(),
Capabilities :: map(),
Result :: ok | {error, Reason},
Reason :: term().
update_agent_capabilities(AgentId, Capabilities) ->
gen_server:call(?SERVER, {update_agent_capabilities, AgentId, Capabilities}).
%% Broadcast a heartbeat to check all agents
broadcast_heartbeat() ->
gen_server:cast(?SERVER, broadcast_heartbeat).
%% gen_server callbacks
init(Options) ->
% Start periodic heartbeat
HeartbeatInterval = maps:get(heartbeat_interval, Options, ?DEFAULT_HEARTBEAT_INTERVAL),
HeartbeatRef = schedule_heartbeat(HeartbeatInterval),
{ok, #state{
heartbeat_interval = HeartbeatInterval,
heartbeat_ref = HeartbeatRef
}}.
handle_call({register_agent, AgentId, Pid, Capabilities}, _From, State) ->
% Monitor the agent process to detect crashes
MonitorRef = erlang:monitor(process, Pid),
% Store agent information
Now = os:timestamp(),
Status = active,
AgentInfo = #{
pid => Pid,
monitor_ref => MonitorRef,
timestamp => Now,
capabilities => Capabilities,
status => Status
},
% Update agents map
NewAgents = maps:put(AgentId, AgentInfo, State#state.agents),
% Update capabilities index
NewCapabilitiesIndex = update_capabilities_index(AgentId, Capabilities, State#state.capabilities_index),
{reply, ok, State#state{
agents = NewAgents,
capabilities_index = NewCapabilitiesIndex
}};
handle_call({unregister_agent, AgentId}, _From, State) ->
% Remove agent and update capabilities index
{NewAgents, NewCapabilitiesIndex} = case maps:find(AgentId, State#state.agents) of
{ok, AgentInfo} ->
% Demonitor the agent process
erlang:demonitor(maps:get(monitor_ref, AgentInfo, make_ref()), [flush]),
% Remove from agents map
NextAgents = maps:remove(AgentId, State#state.agents),
% Remove from capabilities index
NextCapabilitiesIndex = remove_from_capabilities_index(
AgentId,
maps:get(capabilities, AgentInfo, #{}),
State#state.capabilities_index
),
{NextAgents, NextCapabilitiesIndex};
error ->
{State#state.agents, State#state.capabilities_index}
end,
{reply, ok, State#state{
agents = NewAgents,
capabilities_index = NewCapabilitiesIndex
}};
handle_call({find_agent, AgentId}, _From, State) ->
% Find an agent by ID
Result = case maps:find(AgentId, State#state.agents) of
{ok, AgentInfo} -> {ok, format_agent_info(AgentId, AgentInfo)};
error -> {error, {agent_not_found, AgentId}}
end,
{reply, Result, State};
handle_call({find_by_capability, Capability}, _From, State) ->
% Find agents by capability
AgentIds = case maps:find(Capability, State#state.capabilities_index) of
{ok, Ids} -> Ids;
error -> []
end,
% Gather agent information
AgentInfoList = [
format_agent_info(Id, Info)
|| Id <- AgentIds,
{ok, Info} <- [maps:find(Id, State#state.agents)]
],
{reply, {ok, AgentInfoList}, State};
handle_call(find_all, _From, State) ->
% Return all registered agents
AgentInfoList = [
format_agent_info(Id, Info)
|| {Id, Info} <- maps:to_list(State#state.agents)
],
{reply, {ok, AgentInfoList}, State};
handle_call({get_agent_info, AgentId}, _From, State) ->
% Get detailed agent information
Result = case maps:find(AgentId, State#state.agents) of
{ok, AgentInfo} -> {ok, format_agent_info(AgentId, AgentInfo)};
error -> {error, {agent_not_found, AgentId}}
end,
{reply, Result, State};
handle_call({update_agent_status, AgentId, Status}, _From, State) ->
% Update an agent's status
{Result, NewState} = case maps:find(AgentId, State#state.agents) of
{ok, AgentInfo} ->
% Update the status
UpdatedInfo = AgentInfo#{status => Status},
NewAgents = maps:put(AgentId, UpdatedInfo, State#state.agents),
{ok, State#state{agents = NewAgents}};
error ->
{{error, {agent_not_found, AgentId}}, State}
end,
{reply, Result, NewState};
handle_call({update_agent_capabilities, AgentId, NewCapabilities}, _From, State) ->
% Update an agent's capabilities
{Result, NewState} = case maps:find(AgentId, State#state.agents) of
{ok, AgentInfo} ->
% Get the old capabilities
OldCapabilities = maps:get(capabilities, AgentInfo, #{}),
% Update the capabilities
UpdatedInfo = AgentInfo#{capabilities => NewCapabilities},
NewAgents = maps:put(AgentId, UpdatedInfo, State#state.agents),
% Update capabilities index
NewIndex = update_capabilities_index_for_change(
AgentId,
OldCapabilities,
NewCapabilities,
State#state.capabilities_index
),
{ok, State#state{
agents = NewAgents,
capabilities_index = NewIndex
}};
error ->
{{error, {agent_not_found, AgentId}}, State}
end,
{reply, Result, NewState};
handle_call(_Request, _From, State) ->
{reply, {error, unknown_call}, State}.
handle_cast(broadcast_heartbeat, State) ->
% Check all agents for liveness
{NewAgents, NewCapabilitiesIndex} = check_agent_liveness(
State#state.agents,
State#state.capabilities_index
),
% Schedule next heartbeat
NewHeartbeatRef = schedule_heartbeat(State#state.heartbeat_interval),
{noreply, State#state{
agents = NewAgents,
capabilities_index = NewCapabilitiesIndex,
heartbeat_ref = NewHeartbeatRef
}};
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info({'DOWN', MonitorRef, process, _Pid, _Reason}, State) ->
% Find the agent with this monitor reference
AgentId = find_agent_by_monitor_ref(MonitorRef, State#state.agents),
% Remove the agent if found
{NewAgents, NewCapabilitiesIndex} = case AgentId of
undefined ->
{State#state.agents, State#state.capabilities_index};
Id ->
% Get agent info
AgentInfo = maps:get(Id, State#state.agents),
% Remove from agents map
NextAgents = maps:remove(Id, State#state.agents),
% Remove from capabilities index
NextCapabilitiesIndex = remove_from_capabilities_index(
Id,
maps:get(capabilities, AgentInfo, #{}),
State#state.capabilities_index
),
{NextAgents, NextCapabilitiesIndex}
end,
{noreply, State#state{
agents = NewAgents,
capabilities_index = NewCapabilitiesIndex
}};
handle_info(cleanup_stale_agents, State) ->
% Check and remove stale agents
{NewAgents, NewCapabilitiesIndex} = cleanup_stale_agents(
State#state.agents,
State#state.capabilities_index
),
{noreply, State#state{
agents = NewAgents,
capabilities_index = NewCapabilitiesIndex
}};
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, #state{heartbeat_ref = HeartbeatRef}) ->
% Cancel the heartbeat timer if it exists
case HeartbeatRef of
undefined -> ok;
_ -> erlang:cancel_timer(HeartbeatRef)
end,
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% Internal Functions
%% Format agent information for external use
format_agent_info(AgentId, AgentInfo) ->
#{
id => AgentId,
pid => maps:get(pid, AgentInfo),
capabilities => maps:get(capabilities, AgentInfo, #{}),
status => maps:get(status, AgentInfo, unknown),
last_seen => maps:get(timestamp, AgentInfo)
}.
%% Update capabilities index when adding an agent
update_capabilities_index(AgentId, Capabilities, Index) ->
CapabilityKeys = case Capabilities of
Cap when is_map(Cap) -> maps:keys(Cap);
Cap when is_list(Cap) -> Cap;
_ -> []
end,
lists:foldl(
fun(Capability, AccIndex) ->
AgentIds = case maps:find(Capability, AccIndex) of
{ok, Ids} -> Ids;
error -> []
end,
maps:put(Capability, [AgentId | AgentIds], AccIndex)
end,
Index,
CapabilityKeys
).
%% Remove an agent from the capabilities index
remove_from_capabilities_index(AgentId, Capabilities, Index) ->
CapabilityKeys = case Capabilities of
Cap when is_map(Cap) -> maps:keys(Cap);
Cap when is_list(Cap) -> Cap;
_ -> []
end,
lists:foldl(
fun(Capability, AccIndex) ->
case maps:find(Capability, AccIndex) of
{ok, Ids} ->
NewIds = lists:delete(AgentId, Ids),
case NewIds of
[] -> maps:remove(Capability, AccIndex);
_ -> maps:put(Capability, NewIds, AccIndex)
end;
error ->
AccIndex
end
end,
Index,
CapabilityKeys
).
%% Update capabilities index when an agent's capabilities change
update_capabilities_index_for_change(AgentId, OldCapabilities, NewCapabilities, Index) ->
% Convert capabilities to lists of keys
OldKeys = case OldCapabilities of
Old when is_map(Old) -> maps:keys(Old);
Old when is_list(Old) -> Old;
_ -> []
end,
NewKeys = case NewCapabilities of
New when is_map(New) -> maps:keys(New);
New when is_list(New) -> New;
_ -> []
end,
% Find keys to remove and add
KeysToRemove = OldKeys -- NewKeys,
KeysToAdd = NewKeys -- OldKeys,
% Remove agent from old capabilities
IndexAfterRemove = lists:foldl(
fun(Capability, AccIndex) ->
case maps:find(Capability, AccIndex) of
{ok, Ids} ->
NewIds = lists:delete(AgentId, Ids),
case NewIds of
[] -> maps:remove(Capability, AccIndex);
_ -> maps:put(Capability, NewIds, AccIndex)
end;
error ->
AccIndex
end
end,
Index,
KeysToRemove
),
% Add agent to new capabilities
lists:foldl(
fun(Capability, AccIndex) ->
AgentIds = case maps:find(Capability, AccIndex) of
{ok, Ids} -> Ids;
error -> []
end,
maps:put(Capability, [AgentId | AgentIds], AccIndex)
end,
IndexAfterRemove,
KeysToAdd
).
%% Find an agent by monitor reference
find_agent_by_monitor_ref(MonitorRef, Agents) ->
maps:fold(
fun(AgentId, Info, Acc) ->
case maps:get(monitor_ref, Info, undefined) of
MonitorRef -> AgentId;
_ -> Acc
end
end,
undefined,
Agents
).
%% Check agent liveness and update status
check_agent_liveness(Agents, CapabilitiesIndex) ->
% Current timestamp
Now = os:timestamp(),
Threshold = 60 * 1000000, % 60 seconds in microseconds
% Identify agents to mark as inactive
{ActiveAgents, InactiveAgentIds} = maps:fold(
fun(AgentId, Info, {ActiveAcc, InactiveAcc}) ->
Pid = maps:get(pid, Info),
TimeDiff = timer:now_diff(Now, maps:get(timestamp, Info)),
% Check if the process is alive
IsAlive = case is_process_alive(Pid) of
true -> TimeDiff < Threshold;
false -> false
end,
case IsAlive of
true ->
% Update timestamp for active agents
NewInfo = Info#{timestamp => Now},
{maps:put(AgentId, NewInfo, ActiveAcc), InactiveAcc};
false ->
% Mark for removal
{ActiveAcc, [AgentId | InactiveAcc]}
end
end,
{#{}, []},
Agents
),
% Remove inactive agents from capabilities index
NewCapabilitiesIndex = lists:foldl(
fun(AgentId, Index) ->
case maps:find(AgentId, Agents) of
{ok, Info} ->
Capabilities = maps:get(capabilities, Info, #{}),
remove_from_capabilities_index(AgentId, Capabilities, Index);
error ->
Index
end
end,
CapabilitiesIndex,
InactiveAgentIds
),
{ActiveAgents, NewCapabilitiesIndex}.
%% Cleanup stale agents
cleanup_stale_agents(Agents, CapabilitiesIndex) ->
% Current timestamp
Now = os:timestamp(),
Threshold = 600 * 1000000, % 10 minutes in microseconds
% Identify agents to remove
{RemainingAgents, StaleAgentIds} = maps:fold(
fun(AgentId, Info, {RemAcc, StaleAcc}) ->
TimeDiff = timer:now_diff(Now, maps:get(timestamp, Info)),
case TimeDiff < Threshold of
true -> {maps:put(AgentId, Info, RemAcc), StaleAcc};
false -> {RemAcc, [AgentId | StaleAcc]}
end
end,
{#{}, []},
Agents
),
% Remove stale agents from capabilities index
NewCapabilitiesIndex = lists:foldl(
fun(AgentId, Index) ->
case maps:find(AgentId, Agents) of
{ok, Info} ->
Capabilities = maps:get(capabilities, Info, #{}),
remove_from_capabilities_index(AgentId, Capabilities, Index);
error ->
Index
end
end,
CapabilitiesIndex,
StaleAgentIds
),
{RemainingAgents, NewCapabilitiesIndex}.
%% Schedule the next heartbeat
schedule_heartbeat(Interval) ->
erlang:send_after(Interval, self(), broadcast_heartbeat).