use crate::graph::{SkillScope, SkillTrigger};
use car_ir::json_extract::extract_json_array as extract_json_array_from_text;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DistilledSkill {
pub name: String,
pub description: String,
pub when_to_apply: String,
pub scope: SkillScope,
pub source: String,
pub domain: String,
pub trigger: SkillTrigger,
#[serde(default)]
pub code: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TraceEvent {
#[serde(default)]
pub kind: String,
#[serde(default)]
pub action_id: Option<String>,
#[serde(default)]
pub tool: Option<String>,
#[serde(default)]
pub data: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state_before: Option<std::collections::HashMap<String, Value>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state_after: Option<std::collections::HashMap<String, Value>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reward: Option<f64>,
}
pub fn partition_trace(events: &[TraceEvent]) -> (Vec<&TraceEvent>, Vec<&TraceEvent>) {
let mut successes = Vec::new();
let mut failures = Vec::new();
for event in events {
match event.kind.as_str() {
"action_succeeded" => successes.push(event),
"action_failed" | "action_rejected" | "policy_violation" => failures.push(event),
_ => {} }
}
(successes, failures)
}
pub fn success_distillation_prompt(events: &[&TraceEvent]) -> String {
let trace_summary = events
.iter()
.map(|e| {
let tool = e.tool.as_deref().unwrap_or("?");
let action = e.action_id.as_deref().unwrap_or("?");
format!("- [{}] tool={}, data={}", action, tool, e.data)
})
.collect::<Vec<_>>()
.join("\n");
format!(
r#"Analyze this successful execution trace and extract reusable skills.
## Trace
{trace_summary}
## Instructions
Extract 1-3 skills from this successful execution. For each skill, identify:
1. The critical decision point or pattern that led to success
2. A generalizable principle (not specific to this exact task)
3. When this skill should be applied
Output a JSON array of skills:
```json
[
{{
"name": "short_descriptive_name",
"description": "The principle or strategy (1-2 sentences)",
"when_to_apply": "Condition when this skill is useful",
"scope": "global" or {{"domain": "domain_name"}},
"source": "success",
"domain": "",
"trigger": {{"persona": "", "url_pattern": "", "task_keywords": []}},
"code": ""
}}
]
```
Output ONLY the JSON array, no other text."#
)
}
pub fn failure_distillation_prompt(events: &[&TraceEvent]) -> String {
let trace_summary = events
.iter()
.map(|e| {
let tool = e.tool.as_deref().unwrap_or("?");
let action = e.action_id.as_deref().unwrap_or("?");
let error = e.data.get("error").and_then(|v| v.as_str()).unwrap_or("");
format!(
"- [{}] tool={}, error={}, data={}",
action, tool, error, e.data
)
})
.collect::<Vec<_>>()
.join("\n");
format!(
r#"Analyze this failed execution trace and extract preventive skills.
## Failed Actions
{trace_summary}
## Instructions
For each failure pattern, identify:
1. The failure point and what went wrong
2. The flawed reasoning that led to the failure
3. The correct approach that should have been taken
4. A preventive principle to avoid this in the future
Output a JSON array of failure-lesson skills:
```json
[
{{
"name": "prevent_descriptive_name",
"description": "Preventive principle (1-2 sentences)",
"when_to_apply": "Condition when this lesson applies",
"scope": "global" or {{"domain": "domain_name"}},
"source": "failure",
"domain": "",
"trigger": {{"persona": "", "url_pattern": "", "task_keywords": []}},
"code": ""
}}
]
```
Output ONLY the JSON array, no other text."#
)
}
pub fn parse_skills(response: &str) -> Vec<DistilledSkill> {
let json_str = extract_json_array_from_text(response).unwrap_or_else(|| response.to_string());
match serde_json::from_str::<Vec<DistilledSkill>>(&json_str) {
Ok(skills) => skills,
Err(_) => Vec::new(), }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn partition_separates_success_and_failure() {
let events = vec![
TraceEvent {
kind: "action_succeeded".into(),
action_id: Some("a1".into()),
tool: Some("search".into()),
data: Value::Null,
..Default::default()
},
TraceEvent {
kind: "action_failed".into(),
action_id: Some("a2".into()),
tool: Some("shell".into()),
data: Value::Null,
..Default::default()
},
TraceEvent {
kind: "action_succeeded".into(),
action_id: Some("a3".into()),
tool: Some("read".into()),
data: Value::Null,
..Default::default()
},
TraceEvent {
kind: "state_changed".into(),
action_id: None,
tool: None,
data: Value::Null,
..Default::default()
},
];
let (succ, fail) = partition_trace(&events);
assert_eq!(succ.len(), 2);
assert_eq!(fail.len(), 1);
}
#[test]
fn parse_skills_from_json() {
let json = r#"[{"name":"verify_before_act","description":"Always verify state before modifying","when_to_apply":"Before any write operation","scope":"global","source":"failure","domain":"","trigger":{"persona":"","url_pattern":"","task_keywords":[]},"code":""}]"#;
let skills = parse_skills(json);
assert_eq!(skills.len(), 1);
assert_eq!(skills[0].name, "verify_before_act");
assert_eq!(skills[0].scope, SkillScope::Global);
}
#[test]
fn parse_skills_from_markdown() {
let response = "Here are the skills:\n```json\n[{\"name\":\"test\",\"description\":\"d\",\"when_to_apply\":\"w\",\"scope\":\"global\",\"source\":\"success\",\"domain\":\"\",\"trigger\":{\"persona\":\"\",\"url_pattern\":\"\",\"task_keywords\":[]},\"code\":\"\"}]\n```";
let skills = parse_skills(response);
assert_eq!(skills.len(), 1);
}
#[test]
fn parse_empty_on_bad_json() {
let skills = parse_skills("not json at all");
assert!(skills.is_empty());
}
#[test]
fn success_prompt_includes_trace() {
let events = vec![TraceEvent {
kind: "action_succeeded".into(),
action_id: Some("a1".into()),
tool: Some("search".into()),
data: serde_json::json!({"query": "test"}),
..Default::default()
}];
let refs: Vec<&TraceEvent> = events.iter().collect();
let prompt = success_distillation_prompt(&refs);
assert!(prompt.contains("search"));
assert!(prompt.contains("JSON array"));
}
}