Current section

Files

Jump to
macula_neuroevolution src liquid_conglomerate task_silo task_silo.erl
Raw

src/liquid_conglomerate/task_silo/task_silo.erl

%% @doc Task Silo - Evolution optimization controller for neuroevolution.
%%
%% Part of the Liquid Conglomerate v2 architecture. The Task Silo optimizes
%% evolution hyperparameters to maximize fitness improvement.
%%
%% == Hierarchical Levels ==
%%
%% - L0 (Defaults): Safe starting parameters - ALWAYS ACTIVE
%% - L1 (Tactical): Per-generation adjustments based on recent fitness
%% - L2 (Strategic): Multi-generation learning via LTC network (future)
%%
%% == Time Constant ==
%%
%% Ï„ = 50 (slow adaptation for stable evolution)
%%
%% == Usage ==
%%
%% %% Start task silo
%% {ok, Pid} = task_silo:start_link(#{
%% enabled_levels => [l0, l1],
%% stagnation_threshold => 5
%% }),
%%
%% %% Get recommended parameters after a generation
%% Stats = #{best_fitness => 0.85, improvement => 0.02, stagnation => 0},
%% #{mutation_rate := MR} = task_silo:get_recommendations(Pid, Stats).
%%
%% @author Macula.io
%% @copyright 2025 Macula.io
-module(task_silo).
-behaviour(gen_server).
-include("meta_controller.hrl").
%% API
-export([
start_link/0,
start_link/1,
get_recommendations/1,
get_recommendations/2,
update_stats/2,
get_state/1,
set_enabled_levels/2,
set_l2_guidance/2,
reset/1
]).
%% gen_server callbacks
-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3
]).
-define(SERVER, ?MODULE).
-define(TIME_CONSTANT, 50). % tau = 50 generations (slow)
-record(state, {
%% Configuration
enabled_levels :: [l0 | l1 | l2],
realm :: binary(), % Realm for event publishing
%% L2 Configuration
%% When true, task_silo queries meta_controller for L2 guidance
l2_enabled :: boolean(),
%% Current parameters (output)
current_params :: map(),
%% L1 Tactical state (evaluation-centric)
total_evaluations :: non_neg_integer(), %% PRIMARY: Total evaluations (replaces generation)
cohort :: non_neg_integer(), %% SECONDARY: Cohort number for lineage (formerly generation)
fitness_history :: [float()],
improvement_history :: [float()],
history_size :: pos_integer(),
%% Velocity-based stagnation detection (replaces counter-based)
velocity_state :: velocity_state(),
%% L1 Tactical tuning factors
exploration_boost :: float(), % Based on stagnation_severity from velocity
exploitation_boost :: float(), % Increased when improving
%% Previous state for change detection
prev_exploration_boost :: float(),
prev_stagnation_severity :: float(),
%% L2 Strategic guidance (controls L1 behavior)
%% When L2 is enabled, these values come from meta_controller
%% When L2 is disabled, defaults from ?L2_GUIDANCE_DEFAULTS are used
l2_guidance :: l2_guidance()
}).
%%% ============================================================================
%%% API Functions
%%% ============================================================================
%% @doc Start the task silo with default configuration.
-spec start_link() -> {ok, pid()} | ignore | {error, term()}.
start_link() ->
start_link(#{}).
%% @doc Start the task silo with custom configuration.
%%
%% Options:
%% - enabled_levels: [l0, l1, l2] (default: [l0, l1])
%% - stagnation_threshold: Generations without improvement before boosting (default: 5)
%% - history_size: Number of generations to track (default: 20)
-spec start_link(map()) -> {ok, pid()} | ignore | {error, term()}.
start_link(Config) ->
gen_server:start_link({local, ?SERVER}, ?MODULE, Config, []).
%% @doc Get recommended parameters based on current evolution state.
%%
%% Stats should include:
%% - best_fitness: Current best fitness
%% - avg_fitness: Average population fitness
%% - improvement: Fitness improvement from last generation
%% - generation: Current generation number
%%
%% Returns a map of recommended hyperparameters.
-spec get_recommendations(pid(), map()) -> map().
get_recommendations(Pid, Stats) ->
gen_server:call(Pid, {get_recommendations, Stats}).
%% @doc Get recommendations from the registered server using cached state.
-spec get_recommendations(pid()) -> map().
get_recommendations(Pid) ->
gen_server:call(Pid, get_recommendations).
%% @doc Update internal state with generation statistics.
-spec update_stats(pid(), map()) -> ok.
update_stats(Pid, Stats) ->
gen_server:cast(Pid, {update_stats, Stats}).
%% @doc Get current silo state for debugging/monitoring.
-spec get_state(pid()) -> map().
get_state(Pid) ->
gen_server:call(Pid, get_state).
%% @doc Set which levels are enabled.
-spec set_enabled_levels(pid(), [l0 | l1 | l2]) -> ok.
set_enabled_levels(Pid, Levels) ->
gen_server:cast(Pid, {set_enabled_levels, Levels}).
%% @doc Set L2 guidance from meta_controller.
%%
%% Called by meta_controller to update the L1 control parameters.
%% This is the L2→L1 communication channel.
-spec set_l2_guidance(pid(), l2_guidance()) -> ok.
set_l2_guidance(Pid, Guidance) when is_record(Guidance, l2_guidance) ->
gen_server:cast(Pid, {set_l2_guidance, Guidance}).
%% @doc Reset the silo state (for new training session).
-spec reset(pid()) -> ok.
reset(Pid) ->
gen_server:cast(Pid, reset).
%%% ============================================================================
%%% gen_server Callbacks
%%% ============================================================================
init(Config) ->
EnabledLevels = maps:get(enabled_levels, Config, [l0, l1]),
HistorySize = maps:get(history_size, Config, 20),
Realm = maps:get(realm, Config, <<"default">>),
L2Enabled = maps:get(l2_enabled, Config, false),
%% Velocity-based stagnation configuration
VelocityThreshold = maps:get(velocity_threshold, Config, 0.001),
VelocityWindowSize = maps:get(velocity_window_size, Config, 10),
%% Start with L0 defaults
DefaultParams = task_l0_defaults:get_defaults(),
%% Initialize L2 guidance with defaults
%% When L2 is enabled, these will be updated by querying meta_controller
L2Guidance = ?L2_GUIDANCE_DEFAULTS,
%% Initialize velocity state for evaluation-centric stagnation detection
VelocityState = #velocity_state{
improvement_window = [],
window_size = VelocityWindowSize,
current_velocity = 0.0,
velocity_threshold = VelocityThreshold,
fitness_checkpoints = [],
stagnation_severity = 0.0,
last_total_evaluations = 0
},
State = #state{
enabled_levels = EnabledLevels,
realm = Realm,
l2_enabled = L2Enabled,
current_params = DefaultParams,
total_evaluations = 0,
cohort = 0,
fitness_history = [],
improvement_history = [],
history_size = HistorySize,
velocity_state = VelocityState,
exploration_boost = 0.0,
exploitation_boost = 0.0,
prev_exploration_boost = 0.0,
prev_stagnation_severity = 0.0,
l2_guidance = L2Guidance
},
error_logger:info_msg("[task_silo] Started with levels=~p, velocity_threshold=~.4f, l2_enabled=~p~n",
[EnabledLevels, VelocityThreshold, L2Enabled]),
{ok, State}.
handle_call({get_recommendations, Stats}, _From, State) ->
%% Query L2 for guidance if enabled
StateWithL2 = maybe_query_l2_guidance(Stats, State),
%% Update state with new stats and compute recommendations
NewState = update_internal_state(Stats, StateWithL2),
Params = compute_recommendations(NewState),
{reply, Params, NewState#state{current_params = Params}};
handle_call(get_recommendations, _From, State) ->
{reply, State#state.current_params, State};
handle_call(get_state, _From, State) ->
L2Guidance = State#state.l2_guidance,
VelocityState = State#state.velocity_state,
StateMap = #{
enabled_levels => State#state.enabled_levels,
current_params => State#state.current_params,
%% PRIMARY: Evaluation-centric metrics
total_evaluations => State#state.total_evaluations,
%% SECONDARY: Cohort number for backward compatibility
cohort => State#state.cohort,
generation => State#state.cohort, %% Deprecated alias
%% Velocity-based stagnation metrics
improvement_velocity => VelocityState#velocity_state.current_velocity,
stagnation_severity => VelocityState#velocity_state.stagnation_severity,
velocity_threshold => VelocityState#velocity_state.velocity_threshold,
%% L1 tactical state
exploration_boost => State#state.exploration_boost,
exploitation_boost => State#state.exploitation_boost,
fitness_history_size => length(State#state.fitness_history),
%% L2 guidance for visualization
l2_guidance => #{
aggression_factor => L2Guidance#l2_guidance.aggression_factor,
exploration_step => L2Guidance#l2_guidance.exploration_step,
stagnation_sensitivity => L2Guidance#l2_guidance.stagnation_sensitivity,
topology_aggression => L2Guidance#l2_guidance.topology_aggression,
exploitation_weight => L2Guidance#l2_guidance.exploitation_weight
}
},
{reply, StateMap, State};
handle_call(_Request, _From, State) ->
{reply, {error, unknown_request}, State}.
handle_cast({update_stats, Stats}, State) ->
NewState = update_internal_state(Stats, State),
{noreply, NewState};
handle_cast({set_enabled_levels, Levels}, State) ->
error_logger:info_msg("[task_silo] Enabled levels changed to ~p~n", [Levels]),
{noreply, State#state{enabled_levels = Levels}};
handle_cast({set_l2_guidance, Guidance}, State) ->
%% Log significant changes in L2 guidance
OldGuidance = State#state.l2_guidance,
case significant_guidance_change(OldGuidance, Guidance) of
true ->
error_logger:info_msg(
"[task_silo] L2 guidance updated: aggression=~.2f, exploration_step=~.2f, "
"topology_aggression=~.2f~n",
[Guidance#l2_guidance.aggression_factor,
Guidance#l2_guidance.exploration_step,
Guidance#l2_guidance.topology_aggression]
);
false ->
ok
end,
{noreply, State#state{l2_guidance = Guidance}};
handle_cast(reset, State) ->
DefaultParams = task_l0_defaults:get_defaults(),
%% Reset velocity state but preserve threshold configuration
OldVelocityState = State#state.velocity_state,
NewVelocityState = #velocity_state{
improvement_window = [],
window_size = OldVelocityState#velocity_state.window_size,
current_velocity = 0.0,
velocity_threshold = OldVelocityState#velocity_state.velocity_threshold,
fitness_checkpoints = [],
stagnation_severity = 0.0,
last_total_evaluations = 0
},
NewState = State#state{
current_params = DefaultParams,
total_evaluations = 0,
cohort = 0,
fitness_history = [],
improvement_history = [],
velocity_state = NewVelocityState,
exploration_boost = 0.0,
exploitation_boost = 0.0,
prev_exploration_boost = 0.0,
prev_stagnation_severity = 0.0,
l2_guidance = ?L2_GUIDANCE_DEFAULTS
},
error_logger:info_msg("[task_silo] Reset to initial state~n"),
{noreply, NewState};
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%% ============================================================================
%%% Internal Functions - State Updates
%%% ============================================================================
%% @private Update internal state with evaluation-centric statistics.
%%
%% Stats should include:
%% - best_fitness: Current best fitness
%% - total_evaluations: Total evaluations so far (PRIMARY)
%% - generation/cohort: Current cohort number (SECONDARY, for lineage)
%% - improvement: Fitness improvement from previous (optional)
update_internal_state(Stats, State) ->
BestFitness = maps:get(best_fitness, Stats, 0.0),
Improvement = maps:get(improvement, Stats, 0.0),
%% PRIMARY: Total evaluations (evaluation-centric progress)
TotalEvaluations = maps:get(total_evaluations, Stats, State#state.total_evaluations),
%% SECONDARY: Cohort number (accept both 'cohort' and 'generation' for backward compat)
Cohort = maps:get(cohort, Stats, maps:get(generation, Stats, State#state.cohort + 1)),
%% Update fitness history
FitnessHistory = lists:sublist(
[BestFitness | State#state.fitness_history],
State#state.history_size
),
%% Update improvement history
ImprovementHistory = lists:sublist(
[Improvement | State#state.improvement_history],
State#state.history_size
),
%% Update velocity state for stagnation detection
NewVelocityState = update_velocity_state(
BestFitness,
TotalEvaluations,
State#state.velocity_state
),
%% Get L2 guidance (controls boost intensity)
L2Guidance = State#state.l2_guidance,
%% Compute L1 tactical boosts from stagnation severity (continuous 0.0-1.0)
StagnationSeverity = NewVelocityState#velocity_state.stagnation_severity,
{ExplorationBoost, ExploitationBoost} = compute_l1_boosts_from_severity(
StagnationSeverity,
ImprovementHistory,
L2Guidance
),
%% Emit intervention events when stagnation_severity changes significantly
maybe_emit_velocity_intervention_event(
State#state.realm,
TotalEvaluations,
Cohort,
State#state.prev_stagnation_severity,
StagnationSeverity,
NewVelocityState#velocity_state.current_velocity
),
State#state{
total_evaluations = TotalEvaluations,
cohort = Cohort,
fitness_history = FitnessHistory,
improvement_history = ImprovementHistory,
velocity_state = NewVelocityState,
exploration_boost = ExplorationBoost,
exploitation_boost = ExploitationBoost,
prev_exploration_boost = ExplorationBoost,
prev_stagnation_severity = StagnationSeverity
}.
%% @private Update velocity state with new fitness checkpoint.
%%
%% Computes improvement velocity as: (delta_fitness / delta_evals) * 1000
%% Then calculates stagnation_severity from velocity vs threshold.
update_velocity_state(BestFitness, TotalEvaluations, VelocityState) ->
#velocity_state{
fitness_checkpoints = OldCheckpoints,
improvement_window = OldWindow,
window_size = WindowSize,
velocity_threshold = VelocityThreshold,
last_total_evaluations = LastEvaluations
} = VelocityState,
%% Skip if no evaluations since last update
case TotalEvaluations > LastEvaluations of
false ->
VelocityState;
true ->
%% Add new checkpoint
NewCheckpoint = {TotalEvaluations, BestFitness},
Checkpoints = lists:sublist([NewCheckpoint | OldCheckpoints], WindowSize + 1),
%% Calculate velocity from previous checkpoint
Velocity = case OldCheckpoints of
[{PrevEvaluations, PrevFitness} | _] when TotalEvaluations > PrevEvaluations ->
DeltaFitness = BestFitness - PrevFitness,
DeltaEvaluations = TotalEvaluations - PrevEvaluations,
%% Velocity = fitness improvement per 1000 evaluations
(DeltaFitness / DeltaEvaluations) * 1000;
_ ->
0.0
end,
%% Update rolling window of velocities
NewWindow = lists:sublist([Velocity | OldWindow], WindowSize),
%% Calculate average velocity
AvgVelocity = case NewWindow of
[] -> 0.0;
Velocities -> lists:sum(Velocities) / length(Velocities)
end,
%% Calculate stagnation severity: 0.0 = healthy, 1.0 = critical
%% severity = clamp((threshold - avg_velocity) / threshold, 0.0, 1.0)
Severity = case VelocityThreshold > 0 of
true ->
RawSeverity = (VelocityThreshold - AvgVelocity) / VelocityThreshold,
max(0.0, min(1.0, RawSeverity));
false ->
0.0
end,
VelocityState#velocity_state{
fitness_checkpoints = Checkpoints,
improvement_window = NewWindow,
current_velocity = Velocity,
stagnation_severity = Severity,
last_total_evaluations = TotalEvaluations
}
end.
%% @private Compute L1 tactical boosts from stagnation severity.
%%
%% Uses continuous stagnation_severity (0.0-1.0) instead of binary counter threshold.
%% L2 guidance controls the exploration_step (intensity of response).
compute_l1_boosts_from_severity(StagnationSeverity, ImprovementHistory, L2Guidance) ->
%% Get L2-controlled exploration step (default: 0.1, range: 0.05-0.5)
ExplorationStep = L2Guidance#l2_guidance.exploration_step,
%% Exploration boost: proportional to stagnation severity
%% At severity=0.5, boost is 0.5 * exploration_step
%% At severity=1.0, boost is exploration_step (max response)
ExplorationBoost = StagnationSeverity * ExplorationStep,
%% Exploitation boost: increases when consistently improving
ExploitationBoost = case length(ImprovementHistory) >= 3 of
true ->
RecentImprovements = lists:sublist(ImprovementHistory, 3),
AvgImprovement = lists:sum(RecentImprovements) / 3,
case AvgImprovement > 0.01 of
true -> min(1.0, AvgImprovement * 10);
false -> 0.0
end;
false ->
0.0
end,
{ExplorationBoost, ExploitationBoost}.
%%% ============================================================================
%%% Internal Functions - Recommendations
%%% ============================================================================
%% @private Compute recommended parameters based on current state.
compute_recommendations(State) ->
#state{enabled_levels = Levels} = State,
%% Start with L0 defaults
L0Params = task_l0_defaults:get_defaults(),
%% Apply L1 tactical adjustments if enabled
L1Params = case lists:member(l1, Levels) of
true -> apply_l1_adjustments(L0Params, State);
false -> L0Params
end,
%% L2 would apply strategic adjustments here (future)
%% L2Params = case lists:member(l2, Levels) of
%% true -> apply_l2_adjustments(L1Params, State);
%% false -> L1Params
%% end,
%% Always apply bounds from L0 (safety net)
task_l0_defaults:apply_bounds(L1Params).
%% @private Apply L1 tactical adjustments.
%%
%% L1 adjusts parameters based on recent evolution dynamics:
%% - Stagnation -> increase mutation rate, add_node_rate (exploration)
%% - Improvement -> decrease mutation rate (exploitation)
%%
%% L2 guidance controls HOW AGGRESSIVE these adjustments are:
%% - aggression_factor: Multiplier for mutation adjustments (0.0-2.0)
%% - exploitation_weight: Balance between explore/exploit (0.2-0.8)
%% - topology_aggression: Multiplier for add_node_rate boost (1.0-3.0)
apply_l1_adjustments(Params, State) ->
#state{
exploration_boost = ExplorationBoost,
exploitation_boost = ExploitationBoost,
l2_guidance = L2Guidance
} = State,
%% Get L2 guidance factors
Aggression = L2Guidance#l2_guidance.aggression_factor,
ExploitWeight = L2Guidance#l2_guidance.exploitation_weight,
TopologyAggression = L2Guidance#l2_guidance.topology_aggression,
%% Net adjustment factor: positive = explore, negative = exploit
%% L2 controls the exploitation weight (how much to value exploitation)
NetFactor = ExplorationBoost - (ExploitationBoost * ExploitWeight),
%% Adjust mutation rate using L2's aggression factor
%% With Aggression=2.0 and NetFactor=1.0: BaseMR * (1 + 2.0) = BaseMR * 3.0 (+200%)
%% With Aggression=0.5 and NetFactor=1.0: BaseMR * (1 + 0.5) = BaseMR * 1.5 (+50%)
BaseMR = maps:get(mutation_rate, Params, 0.10),
AdjustedMR = BaseMR * (1.0 + NetFactor * Aggression),
%% Adjust mutation strength (slightly less aggressive than mutation rate)
BaseMS = maps:get(mutation_strength, Params, 0.30),
AdjustedMS = BaseMS * (1.0 + NetFactor * Aggression * 0.6),
%% Adjust topology mutation using L2's topology aggression
%% With TopologyAggression=3.0: BaseANR * 3.0 (+200% when heavily stagnating)
BaseANR = maps:get(add_node_rate, Params, 0.03),
AdjustedANR = case ExplorationBoost > 0.5 of
true -> BaseANR * TopologyAggression; % L2-controlled boost
false -> BaseANR
end,
%% Adjust selection ratio (tighter when exploiting)
BaseSR = maps:get(selection_ratio, Params, 0.20),
AdjustedSR = BaseSR * (1.0 - ExploitationBoost * 0.2),
Params#{
mutation_rate => AdjustedMR,
mutation_strength => AdjustedMS,
add_node_rate => AdjustedANR,
selection_ratio => AdjustedSR
}.
%%% ============================================================================
%%% Internal Functions - Intervention Events (Velocity-Based)
%%% ============================================================================
%% @private Emit velocity-based intervention events.
%%
%% Uses stagnation_severity (0.0-1.0) instead of counter-based threshold.
%% Events trigger on severity threshold crossings for clearer state transitions.
%%
%% Thresholds:
%% - severity < 0.3: Healthy (no intervention)
%% - severity >= 0.3: Warning (mild intervention)
%% - severity >= 0.6: Active intervention
%% - severity >= 0.9: Critical intervention
maybe_emit_velocity_intervention_event(_Realm, _TotalEvaluations, _Cohort, Prev, Current, _Velocity)
when abs(Prev - Current) < 0.1 ->
%% No significant change in severity
ok;
maybe_emit_velocity_intervention_event(Realm, TotalEvaluations, Cohort, PrevSeverity, CurrentSeverity, Velocity)
when PrevSeverity < 0.3, CurrentSeverity >= 0.3 ->
%% Crossed into warning territory
Event = {task_silo_stagnation_warning, #{
realm => Realm,
total_evaluations => TotalEvaluations,
cohort => Cohort,
stagnation_severity => CurrentSeverity,
improvement_velocity => Velocity,
message => <<"Improvement velocity declining, monitoring closely">>
}},
Topic = neuroevolution_events:topic_intervention(Realm),
neuroevolution_events:publish(Topic, Event),
error_logger:info_msg(
"[task_silo] Evals ~p: STAGNATION WARNING - severity=~.2f, velocity=~.4f~n",
[TotalEvaluations, CurrentSeverity, Velocity]
);
maybe_emit_velocity_intervention_event(Realm, TotalEvaluations, Cohort, PrevSeverity, CurrentSeverity, Velocity)
when PrevSeverity < 0.6, CurrentSeverity >= 0.6 ->
%% Crossed into active intervention
Event = {task_silo_intervention_started, #{
realm => Realm,
total_evaluations => TotalEvaluations,
cohort => Cohort,
stagnation_severity => CurrentSeverity,
improvement_velocity => Velocity,
message => <<"Stagnation detected, boosting exploration">>
}},
Topic = neuroevolution_events:topic_intervention(Realm),
neuroevolution_events:publish(Topic, Event),
error_logger:info_msg(
"[task_silo] Evals ~p: INTERVENTION STARTED - severity=~.2f, velocity=~.4f~n",
[TotalEvaluations, CurrentSeverity, Velocity]
);
maybe_emit_velocity_intervention_event(Realm, TotalEvaluations, Cohort, PrevSeverity, CurrentSeverity, Velocity)
when PrevSeverity < 0.9, CurrentSeverity >= 0.9 ->
%% Crossed into critical intervention
Event = {task_silo_intervention_critical, #{
realm => Realm,
total_evaluations => TotalEvaluations,
cohort => Cohort,
stagnation_severity => CurrentSeverity,
improvement_velocity => Velocity,
message => <<"Critical stagnation, maximum exploration boost">>
}},
Topic = neuroevolution_events:topic_intervention(Realm),
neuroevolution_events:publish(Topic, Event),
error_logger:warning_msg(
"[task_silo] Evals ~p: CRITICAL INTERVENTION - severity=~.2f, velocity=~.4f~n",
[TotalEvaluations, CurrentSeverity, Velocity]
);
maybe_emit_velocity_intervention_event(Realm, TotalEvaluations, Cohort, PrevSeverity, CurrentSeverity, Velocity)
when PrevSeverity >= 0.3, CurrentSeverity < 0.3 ->
%% Recovered to healthy
Event = {task_silo_intervention_ended, #{
realm => Realm,
total_evaluations => TotalEvaluations,
cohort => Cohort,
stagnation_severity => CurrentSeverity,
improvement_velocity => Velocity,
message => <<"Recovered from stagnation, normal operation resumed">>
}},
Topic = neuroevolution_events:topic_intervention(Realm),
neuroevolution_events:publish(Topic, Event),
error_logger:info_msg(
"[task_silo] Evals ~p: INTERVENTION ENDED - severity=~.2f, velocity=~.4f~n",
[TotalEvaluations, CurrentSeverity, Velocity]
);
maybe_emit_velocity_intervention_event(_Realm, _TotalEvaluations, _Cohort, _Prev, _Current, _Velocity) ->
%% Other changes (severity changed but no threshold crossing)
ok.
%%% ============================================================================
%%% Internal Functions - L2 Guidance Helpers
%%% ============================================================================
%% @private Query meta_controller for L2 guidance if L2 is enabled.
%%
%% When L2 is enabled:
%% 1. Check if meta_controller is registered and alive
%% 2. Call meta_controller:get_l1_guidance/2 with generation stats
%% 3. Update state with new L2 guidance
%%
%% When L2 is disabled or meta_controller unavailable:
%% Return state unchanged (using existing defaults)
maybe_query_l2_guidance(_Stats, #state{l2_enabled = false} = State) ->
State;
maybe_query_l2_guidance(Stats, #state{l2_enabled = true, l2_guidance = OldGuidance} = State) ->
case whereis(meta_controller) of
undefined ->
%% meta_controller not running, use defaults
error_logger:warning_msg("[task_silo] L2 enabled but meta_controller not found, using defaults~n"),
State;
Pid when is_pid(Pid) ->
try
NewGuidance = meta_controller:get_l1_guidance(Pid, Stats),
%% Log significant changes
case significant_guidance_change(OldGuidance, NewGuidance) of
true ->
error_logger:info_msg("[task_silo] L2 guidance updated: aggression=~.2f, exploration_step=~.2f~n",
[NewGuidance#l2_guidance.aggression_factor,
NewGuidance#l2_guidance.exploration_step]);
false ->
ok
end,
State#state{l2_guidance = NewGuidance}
catch
_:Reason ->
error_logger:warning_msg("[task_silo] Failed to query L2 guidance: ~p~n", [Reason]),
State
end
end.
%% @private Check if L2 guidance changed significantly (for logging).
significant_guidance_change(Old, New) ->
%% Check if any parameter changed by more than 10%
abs(Old#l2_guidance.aggression_factor - New#l2_guidance.aggression_factor) > 0.1 orelse
abs(Old#l2_guidance.exploration_step - New#l2_guidance.exploration_step) > 0.05 orelse
abs(Old#l2_guidance.topology_aggression - New#l2_guidance.topology_aggression) > 0.2.