use serde::{Deserialize, Serialize};
pub const MAX_SKILLS: usize = 16;
pub const MAX_INSTRUCTION_CHARS: usize = 600;
pub const MAX_NAME_CHARS: usize = 48;
pub const MAX_BLOB_BYTES: usize = 4000;
pub const SKILLS_HEADER: &str = "=== Your skills ===";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Skill {
pub name: String,
pub instructions: String,
}
fn normalize_name(name: &str) -> String {
let collapsed: String = name
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.to_lowercase();
collapsed.chars().take(MAX_NAME_CHARS).collect()
}
fn normalize_instructions(instructions: &str) -> String {
let collapsed: String = instructions
.split(['\n', '\r'])
.map(str::trim)
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ");
collapsed.chars().take(MAX_INSTRUCTION_CHARS).collect()
}
pub fn parse(blob: &str) -> Vec<Skill> {
let raw: Vec<Skill> = serde_json::from_str(blob.trim()).unwrap_or_default();
let mut out: Vec<Skill> = Vec::new();
for s in raw {
let name = normalize_name(&s.name);
let instructions = normalize_instructions(&s.instructions);
if name.is_empty() || instructions.is_empty() {
continue;
}
if let Some(existing) = out.iter_mut().find(|e| e.name == name) {
existing.instructions = instructions;
} else {
out.push(Skill { name, instructions });
}
}
out
}
pub fn serialize(skills: &[Skill]) -> String {
serde_json::to_string(skills).unwrap_or_else(|_| "[]".to_string())
}
fn enforce_bounds(skills: &mut Vec<Skill>) {
if skills.len() > MAX_SKILLS {
let drop = skills.len() - MAX_SKILLS;
skills.drain(..drop);
}
while skills.len() > 1 && serialize(skills).len() > MAX_BLOB_BYTES {
skills.remove(0);
}
}
pub fn upsert(existing: &str, name: &str, instructions: &str) -> String {
let mut skills = parse(existing);
let name = normalize_name(name);
let instructions = normalize_instructions(instructions);
if name.is_empty() || instructions.is_empty() {
return serialize(&skills);
}
if let Some(s) = skills.iter_mut().find(|s| s.name == name) {
s.instructions = instructions;
} else {
skills.push(Skill { name, instructions });
}
enforce_bounds(&mut skills);
serialize(&skills)
}
pub fn remove(existing: &str, name: &str) -> (String, bool) {
let mut skills = parse(existing);
let name = normalize_name(name);
let before = skills.len();
skills.retain(|s| s.name != name);
let removed = skills.len() != before;
(serialize(&skills), removed)
}
pub fn names(blob: &str) -> Vec<String> {
parse(blob).into_iter().map(|s| s.name).collect()
}
pub fn compose_section(blob: &str) -> Option<String> {
let skills = parse(blob);
if skills.is_empty() {
return None;
}
let body = skills
.iter()
.map(|s| format!("• {}: {}", s.name, s.instructions))
.collect::<Vec<_>>()
.join("\n");
Some(format!(
"{SKILLS_HEADER}\nNamed skills you defined for yourself. Invoke one by name when its description fits the task.\n{body}"
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn upsert_adds_to_empty() {
let blob = upsert("", "greet", "say hello warmly");
let skills = parse(&blob);
assert_eq!(skills.len(), 1);
assert_eq!(skills[0].name, "greet");
assert_eq!(skills[0].instructions, "say hello warmly");
}
#[test]
fn upsert_appends_after_existing() {
let blob = upsert("", "a", "first");
let blob = upsert(&blob, "b", "second");
let skills = parse(&blob);
assert_eq!(skills.len(), 2);
assert_eq!(skills[0].name, "a");
assert_eq!(skills[1].name, "b");
}
#[test]
fn upsert_rejects_empty_name_or_instructions() {
let blob = upsert("", "a", "first");
assert_eq!(upsert(&blob, "", "x"), blob);
assert_eq!(upsert(&blob, " ", "x"), blob);
assert_eq!(upsert(&blob, "b", ""), blob);
assert_eq!(upsert(&blob, "b", "\n\r \n"), blob);
assert_eq!(parse(&upsert("", " ", "x")).len(), 0);
}
#[test]
fn upsert_replaces_same_name_in_place() {
let blob = upsert("", "a", "first");
let blob = upsert(&blob, "b", "second");
let blob = upsert(&blob, "a", "first revised");
let skills = parse(&blob);
assert_eq!(skills.len(), 2, "no duplicate name");
assert_eq!(skills[0].name, "a");
assert_eq!(skills[0].instructions, "first revised");
assert_eq!(skills[1].name, "b");
}
#[test]
fn name_normalized_for_dedup() {
let blob = upsert("", "My Skill", "v1");
let blob = upsert(&blob, " MY SKILL ", "v2");
let skills = parse(&blob);
assert_eq!(skills.len(), 1);
assert_eq!(skills[0].name, "my skill");
assert_eq!(skills[0].instructions, "v2");
}
#[test]
fn instructions_collapse_newlines() {
let blob = upsert("", "s", "line one\nline two\r\nline three");
assert_eq!(parse(&blob)[0].instructions, "line one line two line three");
let blob = upsert("", "s", "a\n\n\nb");
assert_eq!(parse(&blob)[0].instructions, "a b");
}
#[test]
fn name_capped_and_instructions_capped() {
let long_name = "x".repeat(200);
let long_instr = "y".repeat(2000);
let blob = upsert("", &long_name, &long_instr);
let s = &parse(&blob)[0];
assert_eq!(s.name.chars().count(), MAX_NAME_CHARS);
assert_eq!(s.instructions.chars().count(), MAX_INSTRUCTION_CHARS);
let blob = upsert("", &"é".repeat(200), &"é".repeat(2000));
let s = &parse(&blob)[0];
assert_eq!(s.name.chars().count(), MAX_NAME_CHARS);
assert_eq!(s.instructions.chars().count(), MAX_INSTRUCTION_CHARS);
}
#[test]
fn upsert_keeps_only_last_max_skills() {
let mut blob = String::new();
for i in 0..(MAX_SKILLS + 3) {
blob = upsert(&blob, &format!("s{i}"), &format!("instr {i}"));
}
let skills = parse(&blob);
assert_eq!(skills.len(), MAX_SKILLS);
assert_eq!(skills[0].name, "s3");
assert_eq!(skills[MAX_SKILLS - 1].name, format!("s{}", MAX_SKILLS + 2));
}
#[test]
fn upsert_caps_blob_bytes_dropping_oldest() {
let mut blob = String::new();
for i in 0..MAX_SKILLS {
blob = upsert(&blob, &format!("s{i}"), &"z".repeat(MAX_INSTRUCTION_CHARS));
}
assert!(blob.len() <= MAX_BLOB_BYTES, "blob is {} bytes", blob.len());
let skills = parse(&blob);
assert!(!skills.is_empty());
assert_eq!(skills.last().unwrap().name, format!("s{}", MAX_SKILLS - 1));
}
#[test]
fn upsert_never_drops_the_newest_skill() {
let mut blob = String::new();
for i in 0..MAX_SKILLS {
blob = upsert(&blob, &format!("s{i}"), &"z".repeat(MAX_INSTRUCTION_CHARS));
}
let newest_instr = "w".repeat(MAX_INSTRUCTION_CHARS);
let blob = upsert(&blob, "newest", &newest_instr);
let skills = parse(&blob);
assert!(skills.iter().any(|s| s.name == "newest" && s.instructions == newest_instr));
assert!(blob.len() <= MAX_BLOB_BYTES);
}
#[test]
fn remove_existing_and_missing() {
let blob = upsert("", "a", "first");
let blob = upsert(&blob, "b", "second");
let (blob2, removed) = remove(&blob, " A ");
assert!(removed);
let skills = parse(&blob2);
assert_eq!(skills.len(), 1);
assert_eq!(skills[0].name, "b");
let (blob3, removed) = remove(&blob2, "nope");
assert!(!removed);
assert_eq!(parse(&blob3).len(), 1);
}
#[test]
fn names_lists_in_order() {
let blob = upsert(&upsert("", "first", "x"), "second", "y");
assert_eq!(names(&blob), vec!["first".to_string(), "second".to_string()]);
assert!(names("").is_empty());
}
#[test]
fn parse_tolerates_garbage_and_is_idempotent() {
assert!(parse("").is_empty());
assert!(parse("not json").is_empty());
assert!(parse("{}").is_empty());
assert!(parse("[]").is_empty());
assert!(parse(r#"[{"name":"","instructions":"x"}]"#).is_empty());
assert!(parse(r#"[{"name":"a","instructions":""}]"#).is_empty());
let blob = upsert(&upsert("", "a", "one"), "b", "two");
assert_eq!(serialize(&parse(&blob)), blob);
let dup = r#"[{"name":"a","instructions":"one"},{"name":"a","instructions":"two"}]"#;
let skills = parse(dup);
assert_eq!(skills.len(), 1);
assert_eq!(skills[0].instructions, "two");
}
#[test]
fn compose_section_none_when_empty() {
assert_eq!(compose_section(""), None);
assert_eq!(compose_section("[]"), None);
assert_eq!(compose_section("garbage"), None);
}
#[test]
fn compose_section_renders_header_and_skills() {
let blob = upsert(&upsert("", "greet", "say hi"), "summarize", "condense text");
let section = compose_section(&blob).unwrap();
assert!(section.starts_with(SKILLS_HEADER));
assert!(section.contains("• greet: say hi"));
assert!(section.ends_with("• summarize: condense text"));
}
#[test]
fn upsert_then_compose_round_trip() {
let blob = upsert("", "alpha", "do alpha things");
let section = compose_section(&blob).unwrap();
assert!(section.contains("alpha"));
assert!(section.contains("do alpha things"));
}
}