%% Agent Definition Behaviour. %% %% This module defines the behaviour that domain applications must implement %% to define an agent's identity and neural network topology. %% %% An agent definition answers the question: "WHO is this agent?" %% %% == Overview == %% %% The agent definition is the simplest of the SDK behaviours. It provides: %% %% %% The network topology is determined by the sensors and actuators registered %% with the agent, but this callback provides a declarative specification that %% can be validated against the actual sensor/actuator counts. %% %% == Implementing an Agent Definition == %% %% ``` %% -module(my_arena_agent). %% -behaviour(agent_definition). %% %% -export([name/0, version/0, network_topology/0]). %% %% name() -> <<"my_arena_agent">>. %% %% version() -> <<"1.0.0">>. %% %% network_topology() -> %% %% 29 inputs from sensors, 2 hidden layers [32, 16], 9 outputs for actuators %% {29, [32, 16], 9}. %% ''' %% %% == Network Topology == %% %% The topology tuple `{Inputs, HiddenLayers, Outputs}' specifies: %% %% %% The agent_bridge validates that the declared topology matches the registered %% sensors and actuators. %% %% == Semantic Versioning == %% %% Version strings should follow semantic versioning (MAJOR.MINOR.PATCH): %% %% %% @author Macula.io %% @copyright 2025 Macula.io %% @see agent_sensor %% @see agent_actuator %% @see agent_bridge -module(agent_definition). %%% ============================================================================ %%% Types %%% ============================================================================ -type agent_name() :: binary(). %% Unique identifier for the agent type. %% Should be a descriptive name like `<<"hex_arena_agent">>'. -type agent_version() :: binary(). %% Semantic version string, e.g., `<<"1.0.0">>'. -type network_topology() :: {pos_integer(), [pos_integer()], pos_integer()}. %% Neural network shape specification: `{Inputs, HiddenLayers, Outputs}'. %% -export_type([ agent_name/0, agent_version/0, network_topology/0 ]). %%% ============================================================================ %%% Behaviour Callbacks %%% ============================================================================ %% Returns the unique name of this agent type. %% %% The name should be descriptive and unique within the application. %% It is used for registration, logging, and identification. %% %% Example: %% ``` %% name() -> <<"hex_arena_agent">>. %% ''' -callback name() -> agent_name(). %% Returns the semantic version of this agent definition. %% %% Version changes should follow semantic versioning: %% %% %% Example: %% ``` %% version() -> <<"1.2.0">>. %% ''' -callback version() -> agent_version(). %% Returns the neural network topology for this agent. %% %% The topology specifies the shape of the neural network: %% %% %% Example: %% ``` %% network_topology() -> %% %% 29 inputs (vision:18 + smell:3 + hearing:4 + internal:4) %% %% 2 hidden layers [32, 16] %% %% 9 outputs (movement:7 + signal:1 + eat:1) %% {29, [32, 16], 9}. %% ''' -callback network_topology() -> network_topology(). %%% ============================================================================ %%% API Functions %%% ============================================================================ -export([ validate/1, get_info/1 ]). %% Validates that a module correctly implements the agent_definition behaviour. %% %% Checks: %% %% %% Returns `ok' if valid, or `{error, Reasons}' with a list of validation errors. -spec validate(Module) -> ok | {error, [Reason]} when Module :: module(), Reason :: term(). validate(Module) -> Checks = [ fun() -> validate_exports(Module) end, fun() -> validate_name(Module) end, fun() -> validate_version(Module) end, fun() -> validate_topology(Module) end ], Errors = lists:filtermap( fun(Check) -> case Check() of ok -> false; {error, Reason} -> {true, Reason} end end, Checks ), case Errors of [] -> ok; _ -> {error, Errors} end. %% Retrieves agent definition info from a module. %% %% Returns a map with name, version, and topology if the module is valid. %% Returns `{error, Reason}' if the module doesn't properly implement the behaviour. -spec get_info(Module) -> {ok, Info} | {error, Reason} when Module :: module(), Info :: #{ name := agent_name(), version := agent_version(), topology := network_topology(), inputs := pos_integer(), hidden_layers := [pos_integer()], outputs := pos_integer() }, Reason :: term(). get_info(Module) -> case validate(Module) of ok -> Name = Module:name(), Version = Module:version(), {Inputs, HiddenLayers, Outputs} = Module:network_topology(), {ok, #{ name => Name, version => Version, topology => {Inputs, HiddenLayers, Outputs}, inputs => Inputs, hidden_layers => HiddenLayers, outputs => Outputs }}; {error, _} = Error -> Error end. %%% ============================================================================ %%% Internal Functions %%% ============================================================================ %% @private validate_exports(Module) -> RequiredExports = [{name, 0}, {version, 0}, {network_topology, 0}], Exports = Module:module_info(exports), Missing = [F || F <- RequiredExports, not lists:member(F, Exports)], case Missing of [] -> ok; _ -> {error, {missing_exports, Missing}} end. %% @private validate_name(Module) -> try Module:name() of Name when is_binary(Name), byte_size(Name) > 0 -> ok; Name when is_binary(Name) -> {error, {invalid_name, empty_binary}}; Other -> {error, {invalid_name, {expected_binary, Other}}} catch _:Reason -> {error, {name_callback_failed, Reason}} end. %% @private validate_version(Module) -> try Module:version() of Version when is_binary(Version), byte_size(Version) > 0 -> %% Basic semantic version validation (X.Y.Z pattern) case re:run(Version, <<"^[0-9]+\\.[0-9]+\\.[0-9]+">>) of {match, _} -> ok; nomatch -> {error, {invalid_version_format, Version}} end; Version when is_binary(Version) -> {error, {invalid_version, empty_binary}}; Other -> {error, {invalid_version, {expected_binary, Other}}} catch _:Reason -> {error, {version_callback_failed, Reason}} end. %% @private validate_topology(Module) -> try Module:network_topology() of {Inputs, HiddenLayers, Outputs} when is_integer(Inputs), Inputs > 0, is_list(HiddenLayers), is_integer(Outputs), Outputs > 0 -> %% Validate hidden layers are all positive integers case lists:all(fun(H) -> is_integer(H) andalso H > 0 end, HiddenLayers) of true -> ok; false -> {error, {invalid_hidden_layers, HiddenLayers}} end; Other -> {error, {invalid_topology, Other}} catch _:Reason -> {error, {topology_callback_failed, Reason}} end.