use crate::config::MemgineConfig;
use crate::engine::MemgineEngine;
use crate::graph::{SkillMeta, SkillOutcome};
use crate::guidance::{ApprovedSkillPack, RuntimeSkillOverlay, RuntimeSkillStats};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ApprovedSkillRuntimeError {
DuplicateSkillTitle { title: String },
UnknownSkillId { skill_id: String },
}
#[derive(Debug, Clone, PartialEq)]
pub struct ApprovedSkillMatch {
pub skill_id: String,
pub skill_name: String,
pub score: f64,
pub meta: SkillMeta,
}
pub struct ApprovedSkillRuntime {
engine: MemgineEngine,
approved_pack: ApprovedSkillPack,
overlay: RuntimeSkillOverlay,
skill_name_to_id: HashMap<String, String>,
skill_id_to_name: HashMap<String, String>,
}
impl ApprovedSkillRuntime {
pub fn new(
approved_pack: ApprovedSkillPack,
config: Option<MemgineConfig>,
) -> Result<Self, ApprovedSkillRuntimeError> {
Self::with_engine(MemgineEngine::new(config), approved_pack)
}
pub fn with_engine(
mut engine: MemgineEngine,
approved_pack: ApprovedSkillPack,
) -> Result<Self, ApprovedSkillRuntimeError> {
let skill_name_to_id = build_skill_name_to_id(&approved_pack)?;
let skill_id_to_name = invert_mapping(&skill_name_to_id);
let loaded_skill_ids = approved_pack.materialize_into_memgine(&mut engine);
let overlay = RuntimeSkillOverlay::new(loaded_skill_ids);
Ok(Self {
engine,
approved_pack,
overlay,
skill_name_to_id,
skill_id_to_name,
})
}
pub fn approved_pack(&self) -> &ApprovedSkillPack {
&self.approved_pack
}
pub fn overlay(&self) -> &RuntimeSkillOverlay {
&self.overlay
}
pub fn overlay_mut(&mut self) -> &mut RuntimeSkillOverlay {
&mut self.overlay
}
pub fn engine(&self) -> &MemgineEngine {
&self.engine
}
pub fn engine_mut(&mut self) -> &mut MemgineEngine {
&mut self.engine
}
pub fn skill_name_for_id(&self, skill_id: &str) -> Option<&str> {
self.skill_id_to_name.get(skill_id).map(String::as_str)
}
pub fn skill_id_for_name(&self, skill_name: &str) -> Option<&str> {
self.skill_name_to_id.get(skill_name).map(String::as_str)
}
pub fn find_skills(
&self,
persona: &str,
url: &str,
task: &str,
max_results: usize,
) -> Vec<ApprovedSkillMatch> {
self.engine
.find_skill(persona, url, task, max_results)
.into_iter()
.filter_map(|(meta, score)| {
let skill_id = self.skill_name_to_id.get(&meta.name)?.clone();
Some(ApprovedSkillMatch {
skill_id,
skill_name: meta.name.clone(),
score,
meta,
})
})
.collect()
}
pub fn record_outcome(
&mut self,
skill_id: &str,
outcome: SkillOutcome,
) -> Result<RuntimeSkillStats, ApprovedSkillRuntimeError> {
let skill_name = self
.skill_id_to_name
.get(skill_id)
.cloned()
.ok_or_else(|| ApprovedSkillRuntimeError::UnknownSkillId {
skill_id: skill_id.to_string(),
})?;
self.engine.report_outcome(&skill_name, outcome.clone());
let stats = self.overlay.record_outcome(skill_id, outcome).clone();
Ok(stats)
}
pub fn record_match_outcome(
&mut self,
selected: &ApprovedSkillMatch,
outcome: SkillOutcome,
) -> Result<RuntimeSkillStats, ApprovedSkillRuntimeError> {
self.record_outcome(&selected.skill_id, outcome)
}
}
fn build_skill_name_to_id(
pack: &ApprovedSkillPack,
) -> Result<HashMap<String, String>, ApprovedSkillRuntimeError> {
let mut mapping = HashMap::new();
for skill in &pack.general_skills {
insert_skill_mapping(&mut mapping, &skill.title, &skill.id)?;
}
for skills in pack.task_specific_skills.values() {
for skill in skills {
insert_skill_mapping(&mut mapping, &skill.title, &skill.id)?;
}
}
Ok(mapping)
}
fn insert_skill_mapping(
mapping: &mut HashMap<String, String>,
title: &str,
id: &str,
) -> Result<(), ApprovedSkillRuntimeError> {
if mapping.insert(title.to_string(), id.to_string()).is_some() {
return Err(ApprovedSkillRuntimeError::DuplicateSkillTitle {
title: title.to_string(),
});
}
Ok(())
}
fn invert_mapping(mapping: &HashMap<String, String>) -> HashMap<String, String> {
mapping
.iter()
.map(|(title, id)| (id.clone(), title.clone()))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::{SkillScope, SkillTrigger};
use crate::guidance::ApprovedSkill;
fn sample_skill(id: &str, title: &str, keyword: &str) -> ApprovedSkill {
ApprovedSkill {
id: id.to_string(),
title: title.to_string(),
description: format!("Description for {title}"),
when_to_apply: format!("Use when task involves {keyword}"),
when_not_to_apply: "Do not use in unrelated flows".to_string(),
scope: SkillScope::Global,
trigger: SkillTrigger {
persona: String::new(),
url_pattern: String::new(),
task_keywords: vec![keyword.to_string()],
structured: None,
},
code: format!("// code for {title}"),
source_candidate_ids: vec!["candidate-1".to_string()],
}
}
#[test]
fn runtime_loads_pack_and_finds_skills() {
let mut pack = ApprovedSkillPack::empty(1);
pack.general_skills
.push(sample_skill("skill-1", "triage_issue", "triage"));
let runtime = ApprovedSkillRuntime::new(pack, None).expect("runtime should load");
let matches = runtime.find_skills("", "", "triage this bug", 3);
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].skill_id, "skill-1");
assert_eq!(matches[0].skill_name, "triage_issue");
assert_eq!(
runtime.overlay().loaded_skill_ids,
vec!["skill-1".to_string()]
);
}
#[test]
fn runtime_records_outcomes_in_overlay_and_engine() {
let mut pack = ApprovedSkillPack::empty(1);
pack.general_skills
.push(sample_skill("skill-1", "triage_issue", "triage"));
let mut runtime = ApprovedSkillRuntime::new(pack, None).expect("runtime should load");
let selected = runtime.find_skills("", "", "triage this bug", 1).remove(0);
let stats = runtime
.record_match_outcome(&selected, SkillOutcome::Fail)
.expect("skill should be tracked");
assert_eq!(stats.fail_count, 1);
assert_eq!(stats.success_count, 0);
let engine_stats = runtime
.engine()
.find_skill("", "", "triage this bug", 1)
.into_iter()
.next()
.expect("skill should still exist")
.0
.stats;
assert_eq!(engine_stats.fail_count, 1);
let pack_skill = &runtime.approved_pack().general_skills[0];
assert_eq!(pack_skill.id, "skill-1");
assert_eq!(pack_skill.title, "triage_issue");
}
#[test]
fn runtime_rejects_duplicate_titles() {
let mut pack = ApprovedSkillPack::empty(1);
pack.general_skills
.push(sample_skill("skill-1", "triage_issue", "triage"));
pack.general_skills
.push(sample_skill("skill-2", "triage_issue", "debug"));
let err = match ApprovedSkillRuntime::new(pack, None) {
Ok(_) => panic!("duplicate titles should fail"),
Err(err) => err,
};
assert_eq!(
err,
ApprovedSkillRuntimeError::DuplicateSkillTitle {
title: "triage_issue".to_string()
}
);
}
}