starfire / lib /autonomy /kernel.rs
system's picture
system HF Staff
Deploy native Starfire runtime from a0db49e
227d301 verified
Raw
History Blame Contribute Delete
11 kB
use serde::{Deserialize, Serialize};
use crate::charge::{
Charge, ChargeKind, ChargeScope, DischargeJudge, ImprovementDirection, OutcomeWitness,
RelativeImprovementJudge, Resolution,
};
use crate::cognitive_cycle::CognitiveCycleState;
use crate::environment::Environment;
use super::broker::{ActionBroker, ActionEnvelope};
use super::budget::{BudgetUsage, ResourceBudget};
use super::goals::{Goal, GoalStatus};
use super::operators::{OperatorContext, OperatorRegistry};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum EpisodeTermination {
Solved,
EnvironmentTerminal,
BudgetExhausted(String),
NoPendingPressure,
NoApplicableOperator,
AuthorityDenied,
ActionCostUnderdeclared,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EpisodeStepRecord {
pub step_index: u64,
pub operator_id: String,
pub action: String,
pub before_progress: f64,
pub after_progress: f64,
pub requested_discharge: f32,
pub accepted_discharge: f32,
pub charge_persistence_after: Option<u32>,
pub environment_evidence: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EpisodeReport {
pub seed: u64,
pub goal: Goal,
pub solved: bool,
pub termination: EpisodeTermination,
pub usage: BudgetUsage,
pub denied_actions: u64,
pub remaining_charge: f32,
pub records: Vec<EpisodeStepRecord>,
}
/// Shadow-only autonomous executive. It can act only through a typed
/// `Environment`, an `ActionBroker`, and independently judged CHARGE discharge.
pub struct AutonomousKernel<E: Environment> {
goal: Goal,
registry: OperatorRegistry<E>,
broker: ActionBroker,
budget: ResourceBudget,
judge: RelativeImprovementJudge,
cycle: CognitiveCycleState,
next_charge_id: u64,
}
impl<E: Environment + 'static> AutonomousKernel<E> {
pub fn new(
goal: Goal,
registry: OperatorRegistry<E>,
broker: ActionBroker,
budget: ResourceBudget,
) -> Self {
Self {
goal,
registry,
broker,
budget,
judge: RelativeImprovementJudge,
cycle: CognitiveCycleState::new(),
next_charge_id: 1,
}
}
pub fn run_episode(&mut self, environment: &mut E, seed: u64) -> EpisodeReport {
self.goal.activate();
self.cycle = CognitiveCycleState::new();
let denied_before = self.broker.denied_attempts();
let mut usage = BudgetUsage::default();
let mut records = Vec::new();
let mut observation = environment.reset(seed);
let mut feedback = environment.objective_feedback();
let termination = loop {
if feedback.solved {
self.goal.status = GoalStatus::Achieved;
break EpisodeTermination::Solved;
}
if self.cycle.pending().is_empty() && !self.admit_goal_pressure(feedback.progress) {
self.goal.status = GoalStatus::Blocked;
break EpisodeTermination::NoPendingPressure;
}
let Some(pressure_index) = self.select_pressure() else {
self.goal.status = GoalStatus::Blocked;
break EpisodeTermination::NoPendingPressure;
};
let charge = self.cycle.pending()[pressure_index].clone();
let available_actions = environment.available_actions();
let context = OperatorContext {
observation: &observation,
available_actions: &available_actions,
objective: &feedback,
charge: &charge,
step_index: usage.steps,
};
let Some(selected) = self.registry.select(&context) else {
self.goal.status = GoalStatus::Blocked;
break EpisodeTermination::NoApplicableOperator;
};
let envelope = ActionEnvelope {
action: selected.proposal.action.clone(),
authority: selected.proposal.authority,
rationale: selected.proposal.rationale.clone(),
};
if self.broker.authorize(&envelope).is_err() {
self.goal.status = GoalStatus::Blocked;
break EpisodeTermination::AuthorityDenied;
}
if let Err(error) = usage.reserve(
self.budget,
selected.proposal.declared_action_cost,
selected.proposal.compute_cost,
) {
self.goal.status = GoalStatus::BudgetExhausted;
break EpisodeTermination::BudgetExhausted(error.to_string());
}
let action_debug = format!("{:?}", selected.proposal.action);
let before = feedback.clone();
let step = environment.act(&selected.proposal.action);
if step.action_cost > selected.proposal.declared_action_cost {
self.goal.status = GoalStatus::Failed;
break EpisodeTermination::ActionCostUnderdeclared;
}
let after = environment.objective_feedback();
let resolution = Resolution {
discharged: selected
.proposal
.requested_discharge
.min(charge.magnitude),
emitted: Vec::new(),
permitted_decay: 0.0,
compute_cost: selected.proposal.compute_cost,
};
let witness = OutcomeWitness::new(
"objective_progress",
before.progress,
after.progress,
ImprovementDirection::HigherIsBetter,
after.evidence.clone(),
);
let judged = self.judge.evaluate(&charge, &resolution, &witness);
let _ = self.cycle.apply_judgment(pressure_index, &judged);
records.push(EpisodeStepRecord {
step_index: usage.steps - 1,
operator_id: selected.operator_id,
action: action_debug,
before_progress: before.progress,
after_progress: after.progress,
requested_discharge: judged.requested,
accepted_discharge: judged.accepted,
charge_persistence_after: self.cycle.pending().first().map(|c| c.persistence),
environment_evidence: after.evidence.clone(),
});
observation = step.observation;
feedback = after;
if feedback.solved {
self.goal.status = GoalStatus::Achieved;
break EpisodeTermination::Solved;
}
if step.terminal {
self.goal.status = GoalStatus::Failed;
break EpisodeTermination::EnvironmentTerminal;
}
};
EpisodeReport {
seed,
goal: self.goal.clone(),
solved: feedback.solved,
termination,
usage,
denied_actions: self.broker.denied_attempts().saturating_sub(denied_before),
remaining_charge: self
.cycle
.pending()
.iter()
.map(|charge| charge.magnitude)
.sum(),
records,
}
}
fn admit_goal_pressure(&mut self, progress: f64) -> bool {
let residual = (1.0 - progress.clamp(0.0, 1.0)) as f32;
if residual <= f32::EPSILON {
return false;
}
let mut charge = Charge::new(
ChargeKind::GoalTension,
vec![residual],
residual,
ChargeScope::Goal(self.goal.description.clone()),
);
charge.id = self.next_charge_id;
self.next_charge_id = self.next_charge_id.saturating_add(1);
self.cycle.admit_charge(charge)
}
fn select_pressure(&self) -> Option<usize> {
self.cycle
.pending()
.iter()
.enumerate()
.max_by(|(left_index, left), (right_index, right)| {
let left_score = left.magnitude * (1.0 + left.persistence as f32);
let right_score = right.magnitude * (1.0 + right.persistence as f32);
left_score
.total_cmp(&right_score)
.then_with(|| right_index.cmp(left_index))
})
.map(|(index, _)| index)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::autonomy::broker::ActionAuthority;
use crate::autonomy::operators::{CognitiveOperator, OperatorProposal};
use crate::environment::hidden_rule::{
HiddenRuleAction, HiddenRuleEnvironment, HiddenRuleObservation,
};
struct EvidenceGuidedOperator;
impl CognitiveOperator<HiddenRuleEnvironment> for EvidenceGuidedOperator {
fn id(&self) -> &str {
"evidence-guided"
}
fn propose(
&mut self,
context: &OperatorContext<'_, HiddenRuleEnvironment>,
) -> Option<OperatorProposal<HiddenRuleAction>> {
let HiddenRuleObservation {
clue,
candidate,
submitted,
} = *context.observation;
let action = if clue.is_none() {
HiddenRuleAction::Inspect
} else if candidate != clue {
HiddenRuleAction::Set(clue.unwrap())
} else if !submitted {
HiddenRuleAction::Submit
} else {
return None;
};
Some(OperatorProposal {
action,
authority: ActionAuthority::ReversibleSandbox,
rationale: "follow independently observed clue".into(),
predicted_effect: "increase objective progress".into(),
expected_utility: 1.0,
requested_discharge: 1.0,
compute_cost: 1,
declared_action_cost: 1,
})
}
}
#[test]
fn bounded_kernel_solves_both_hidden_rule_variants() {
for seed in 0..2 {
let mut registry = OperatorRegistry::new();
registry.register(EvidenceGuidedOperator).unwrap();
let mut kernel = AutonomousKernel::new(
Goal::external(1, "infer and satisfy the hidden rule"),
registry,
ActionBroker::sandbox_only(),
ResourceBudget::new(4, 4, 4),
);
let mut environment = HiddenRuleEnvironment::new();
let report = kernel.run_episode(&mut environment, seed);
assert!(report.solved, "report: {report:?}");
assert_eq!(report.termination, EpisodeTermination::Solved);
assert_eq!(report.usage.steps, 3);
assert_eq!(report.denied_actions, 0);
assert!(report.records.iter().all(|step| step.accepted_discharge >= 0.0));
}
}
}