zeph-core 0.22.1

Core agent loop, configuration, context builder, metrics, and vault for Zeph
Documentation
// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
// SPDX-License-Identifier: MIT OR Apache-2.0

use super::super::{Agent, Channel};

impl<C: Channel> Agent<C> {
    /// Fire-and-forget RL routing head update for the selected skill.
    ///
    /// No-op when `rl_routing_enabled = false` or no RL head is loaded.
    /// Only updates for the first active skill name (the one that was selected).
    pub(crate) fn spawn_rl_head_update(&mut self, outcome: &str) {
        let Some(cfg) = self.services.learning_engine.rl_routing else {
            return;
        };
        if !cfg.enabled {
            return;
        }
        let Some(selected_skill) = self.services.skill.active_skill_names.first().cloned() else {
            return;
        };
        let Some(rl_head) = self.services.skill.rl_head.clone() else {
            return;
        };
        let reward = if outcome == "success" {
            1.0f32
        } else {
            -1.0f32
        };
        let lr = cfg.learning_rate;
        let persist_interval = cfg.persist_interval;
        let memory = self.services.memory.persistence.memory.clone();

        self.try_spawn_learning_task(async move {
            if !rl_head.update(reward, lr) {
                tracing::debug!(
                    skill = selected_skill,
                    "rl_head: no forward cache, skipping update"
                );
                return;
            }
            let update_count = rl_head.update_count();
            if (persist_interval == 0 || update_count % persist_interval == 0)
                && let Some(mem) = memory
            {
                // #5974: snapshot embed_dim/bytes/baseline/update_count under one lock
                // acquisition — this handle may be shared with concurrent sessions, and a
                // second `update()` landing between separate locked reads would otherwise
                // desync the persisted `update_count` column from the blob's own copy.
                let (embed_dim, bytes, baseline, count) = rl_head.persist_snapshot();
                let embed_dim = i64::try_from(embed_dim).unwrap_or(i64::MAX);
                let baseline = f64::from(baseline);
                let count = i64::from(count);
                if let Err(e) = mem
                    .sqlite()
                    .save_routing_head_weights(embed_dim, &bytes, baseline, count)
                    .await
                {
                    tracing::debug!(
                        skill = selected_skill,
                        "rl_head: failed to persist weights: {e:#}"
                    );
                }
            }
        });
    }
}