use crate::{Database, DbResultExt};
use roboticus_core::Result;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DelegationOutcomeRow {
pub id: String,
pub turn_id: String,
pub session_id: String,
pub task_description: String,
pub subtask_count: i64,
pub pattern: String,
pub assigned_agents_json: String,
pub total_duration_ms: Option<i64>,
pub success: bool,
pub quality_score: Option<f64>,
pub skill_utilization_json: Option<String>,
pub retry_count: i32,
pub created_at: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct AgentDelegationStats {
pub agent_id: String,
pub total_delegations: i64,
pub successes: i64,
pub success_rate: f64,
}
pub fn insert_delegation_outcome(db: &Database, row: &DelegationOutcomeRow) -> Result<()> {
let conn = db.conn();
conn.execute(
"INSERT INTO delegation_outcomes \
(id, turn_id, session_id, task_description, subtask_count, pattern, \
assigned_agents_json, total_duration_ms, success, quality_score, \
skill_utilization_json, retry_count, created_at) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, \
COALESCE(NULLIF(?13, ''), datetime('now')))",
rusqlite::params![
row.id,
row.turn_id,
row.session_id,
row.task_description,
row.subtask_count,
row.pattern,
row.assigned_agents_json,
row.total_duration_ms,
row.success as i32,
row.quality_score,
row.skill_utilization_json,
row.retry_count,
row.created_at,
],
)
.db_err()?;
Ok(())
}
pub fn recent_delegation_outcomes(db: &Database, limit: i64) -> Result<Vec<DelegationOutcomeRow>> {
let conn = db.conn();
let mut stmt = conn
.prepare(
"SELECT id, turn_id, session_id, task_description, subtask_count, pattern, \
assigned_agents_json, total_duration_ms, success, quality_score, \
skill_utilization_json, retry_count, created_at \
FROM delegation_outcomes ORDER BY created_at DESC LIMIT ?1",
)
.db_err()?;
let rows = stmt
.query_map(rusqlite::params![limit], |row| {
Ok(DelegationOutcomeRow {
id: row.get(0)?,
turn_id: row.get(1)?,
session_id: row.get(2)?,
task_description: row.get(3)?,
subtask_count: row.get(4)?,
pattern: row.get(5)?,
assigned_agents_json: row.get(6)?,
total_duration_ms: row.get(7)?,
success: row.get::<_, i32>(8)? != 0,
quality_score: row.get(9)?,
skill_utilization_json: row.get(10)?,
retry_count: row.get(11)?,
created_at: row.get(12)?,
})
})
.db_err()?;
rows.collect::<std::result::Result<Vec<_>, _>>().db_err()
}
pub fn delegation_stats_by_agent(db: &Database, hours: i64) -> Result<Vec<AgentDelegationStats>> {
let conn = db.conn();
let mut stmt = conn
.prepare(
"SELECT agent_id, COUNT(*) as total, SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as wins \
FROM ( \
SELECT json_each.value as agent_id, success \
FROM delegation_outcomes, json_each(assigned_agents_json) \
WHERE created_at >= datetime('now', '-' || ?1 || ' hours') \
) \
GROUP BY agent_id",
)
.db_err()?;
let rows = stmt
.query_map(rusqlite::params![hours], |row| {
let total: i64 = row.get(1)?;
let wins: i64 = row.get(2)?;
Ok(AgentDelegationStats {
agent_id: row.get(0)?,
total_delegations: total,
successes: wins,
success_rate: if total > 0 {
wins as f64 / total as f64
} else {
0.0
},
})
})
.db_err()?;
rows.collect::<std::result::Result<Vec<_>, _>>().db_err()
}
#[cfg(test)]
mod tests {
use super::*;
fn test_db() -> Database {
let db = Database::new(":memory:").unwrap();
db.conn()
.execute_batch(
"CREATE TABLE IF NOT EXISTS delegation_outcomes (
id TEXT PRIMARY KEY,
turn_id TEXT NOT NULL,
session_id TEXT NOT NULL,
task_description TEXT NOT NULL,
subtask_count INTEGER NOT NULL DEFAULT 0,
pattern TEXT NOT NULL DEFAULT 'fan-out',
assigned_agents_json TEXT NOT NULL DEFAULT '[]',
total_duration_ms INTEGER,
success INTEGER NOT NULL DEFAULT 0,
quality_score REAL,
skill_utilization_json TEXT,
retry_count INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);",
)
.unwrap();
db
}
#[test]
fn insert_and_retrieve_outcome() {
let db = test_db();
let row = DelegationOutcomeRow {
id: "d-1".into(),
turn_id: "t-1".into(),
session_id: "s-1".into(),
task_description: "test task".into(),
subtask_count: 2,
pattern: "fan-out".into(),
assigned_agents_json: r#"["agent-a","agent-b"]"#.into(),
total_duration_ms: Some(500),
success: true,
quality_score: Some(0.85),
skill_utilization_json: Some(r#"["code-review"]"#.into()),
retry_count: 0,
created_at: String::new(),
};
insert_delegation_outcome(&db, &row).unwrap();
let recent = recent_delegation_outcomes(&db, 10).unwrap();
assert_eq!(recent.len(), 1);
assert_eq!(recent[0].id, "d-1");
assert!(recent[0].success);
assert_eq!(recent[0].retry_count, 0);
assert!(
recent[0]
.skill_utilization_json
.as_ref()
.unwrap()
.contains("code-review")
);
}
#[test]
fn skill_utilization_persists_and_retrieves() {
let db = test_db();
let row = DelegationOutcomeRow {
id: "d-util-1".into(),
turn_id: "t-1".into(),
session_id: "s-1".into(),
task_description: "test utilization".into(),
subtask_count: 1,
pattern: "fan-out".into(),
assigned_agents_json: r#"["agent-a"]"#.into(),
total_duration_ms: Some(100),
success: true,
quality_score: Some(0.9),
skill_utilization_json: Some(r#"["code-review","testing"]"#.into()),
retry_count: 1,
created_at: String::new(),
};
insert_delegation_outcome(&db, &row).unwrap();
let recent = recent_delegation_outcomes(&db, 10).unwrap();
assert_eq!(recent.len(), 1);
assert!(
recent[0]
.skill_utilization_json
.as_ref()
.unwrap()
.contains("code-review")
);
assert_eq!(recent[0].retry_count, 1);
}
#[test]
fn delegation_stats_groups_by_agent() {
let db = test_db();
for (id, success) in [("d-1", true), ("d-2", true), ("d-3", false)] {
let row = DelegationOutcomeRow {
id: id.into(),
turn_id: "t-1".into(),
session_id: "s-1".into(),
task_description: "task".into(),
subtask_count: 1,
pattern: "fan-out".into(),
assigned_agents_json: r#"["agent-alpha"]"#.into(),
total_duration_ms: Some(50),
success,
quality_score: None,
skill_utilization_json: None,
retry_count: 0,
created_at: String::new(),
};
insert_delegation_outcome(&db, &row).unwrap();
}
let stats = delegation_stats_by_agent(&db, 168).unwrap();
assert_eq!(stats.len(), 1);
assert_eq!(stats[0].agent_id, "agent-alpha");
assert_eq!(stats[0].total_delegations, 3);
assert_eq!(stats[0].successes, 2);
assert!((stats[0].success_rate - 2.0 / 3.0).abs() < 0.01);
}
}