use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::entities::message::{Message, MessageRole};
use crate::entities::sampling::SamplingConfig;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RunKind {
Subagent,
Dialogue,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Participant {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub system_message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RunOutcome {
Completed,
Cancelled,
TimedOut,
Failed,
RoundLimit,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SubagentRun {
pub id: Uuid,
pub kind: RunKind,
pub title: String,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub renamed_manually: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub created_at: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub finished_at: Option<DateTime<Utc>>,
pub system_message: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub participants: Vec<Participant>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sampling_override: Option<SamplingConfig>,
pub messages: Vec<Message>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub outcome: Option<RunOutcome>,
#[serde(default, skip_serializing_if = "crate::entities::message::is_zero_u64")]
pub tokens: u64,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub background: bool,
}
impl SubagentRun {
#[cfg(test)]
pub fn fixture(title: &str, texts: &[&str]) -> Self {
let messages = texts
.iter()
.enumerate()
.map(|(i, t)| {
if i % 2 == 0 {
Message::user(*t)
} else {
Message::assistant(*t)
}
})
.collect();
Self {
id: Uuid::new_v4(),
kind: RunKind::Subagent,
title: title.into(),
renamed_manually: false,
name: Some(title.into()),
created_at: Utc::now(),
finished_at: Some(Utc::now()),
system_message: "persona".into(),
sampling_override: None,
messages,
outcome: Some(RunOutcome::Completed),
tokens: 0,
participants: Vec::new(),
background: false,
}
}
#[cfg(test)]
pub fn on_record(self) -> crate::entities::message::ToolCallRecord {
let bare = r#"{"id":"c1","name":"call_subagent","arguments":{"message":"x"},"result":"y"}"#;
let mut rec: crate::entities::message::ToolCallRecord = serde_json::from_str(bare).unwrap();
rec.subagent = Some(Box::new(self));
rec
}
pub fn final_reply(&self) -> Option<&str> {
self.messages
.iter()
.rev()
.find(|m| m.role == MessageRole::Assistant && !m.text.trim().is_empty())
.map(|m| m.text.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::entities::message::{Message, ToolCallRecord};
fn run() -> SubagentRun {
SubagentRun {
id: Uuid::new_v4(),
kind: RunKind::Subagent,
title: "Критик".into(),
renamed_manually: false,
name: Some("Критик".into()),
created_at: Utc::now(),
finished_at: Some(Utc::now()),
system_message: "Ты — критик.".into(),
sampling_override: None,
messages: vec![Message::user("Оцени X."), Message::assistant("X слаб.")],
outcome: Some(RunOutcome::Completed),
tokens: 12,
participants: Vec::new(),
background: false,
}
}
#[test]
fn final_reply_is_the_last_substantive_assistant_text() {
let mut r = run();
assert_eq!(r.final_reply(), Some("X слаб."));
r.messages.push(Message::assistant(" "));
assert_eq!(r.final_reply(), Some("X слаб."));
r.messages.clear();
assert_eq!(r.final_reply(), None);
}
#[test]
fn run_on_a_record_is_additive_and_round_trips() {
let old = r#"{"id":"c1","name":"call_subagent","arguments":{"message":"x"},"result":"y"}"#;
let rec: ToolCallRecord = serde_json::from_str(old).unwrap();
assert!(rec.subagent.is_none());
let json = serde_json::to_string(&rec).unwrap();
assert!(!json.contains("\"subagent\""), "{json}");
let mut with = rec;
with.subagent = Some(Box::new(run()));
let mut msg = Message::assistant("");
msg.tool_calls = vec![with];
let json = serde_json::to_string(&msg).unwrap();
let back: Message = serde_json::from_str(&json).unwrap();
assert_eq!(back, msg);
let run = back.tool_calls[0].subagent.as_ref().unwrap();
assert_eq!(run.messages.len(), 2);
assert_eq!(run.outcome, Some(RunOutcome::Completed));
}
}