use super::*;
#[derive(Clone, Debug)]
pub struct SubAgentConversationRecorder {
conversations: Conversations,
}
impl SubAgentConversationRecorder {
pub fn new(conversations: Conversations) -> Self {
Self { conversations }
}
pub(super) async fn start(
&self,
ctx: &AgentCtx,
agent: &SubAgent,
mode: &'static str,
session: Option<&str>,
req: &CompletionRequest,
resources: Vec<Resource>,
) -> Result<SubAgentConversationLog, BoxError> {
let now_ms = unix_ms();
let session = session.map(str::to_string);
let mut conversation = Conversation {
user: ctx.base.caller,
messages: initial_request_messages(req, now_ms),
resources,
status: ConversationStatus::Working,
period: now_ms / 3600 / 1000,
created_at: now_ms,
updated_at: now_ms,
label: Some(format!("subagent:{}", agent.name.to_ascii_lowercase())),
extra: Some(json!({
"kind": "subagent",
"subagent": agent.name.to_ascii_lowercase(),
"session": session,
"mode": mode,
"parent_agent": ctx.root.agent.clone(),
"context_agent": ctx.base.agent.clone(),
"model": req.model.clone(),
"effort": req.effort,
"tools": agent.tools.clone(),
"tags": agent.tags.clone(),
"has_output_schema": agent.output_schema.is_some(),
})),
..Default::default()
};
let id = self
.conversations
.add_conversation(ConversationRef::from(&conversation))
.await?;
conversation._id = id;
Ok(SubAgentConversationLog {
recorder: self.clone(),
conversation,
persisted_runner_history_len: 0,
replace_initial_input: true,
})
}
}
pub(super) struct SubAgentConversationLog {
recorder: SubAgentConversationRecorder,
pub(super) conversation: Conversation,
persisted_runner_history_len: usize,
replace_initial_input: bool,
}
impl SubAgentConversationLog {
pub(super) fn id(&self) -> u64 {
self.conversation._id
}
pub(super) fn reset_runner_history_cursor(&mut self) {
self.persisted_runner_history_len = 0;
self.replace_initial_input = false;
}
pub(super) async fn mark_status(&mut self, status: ConversationStatus) {
if self.conversation.status == status {
return;
}
self.conversation.status = status;
self.conversation.updated_at = unix_ms();
self.persist().await;
}
pub(super) async fn record_output(
&mut self,
output: &mut AgentOutput,
status: ConversationStatus,
) {
append_runner_history(
&mut self.conversation,
&output.chat_history,
&mut self.persisted_runner_history_len,
&mut self.replace_initial_input,
);
self.conversation.status = status;
self.conversation.usage = output.usage.clone();
self.conversation.artifacts = output.artifacts.clone();
self.conversation.updated_at = unix_ms();
self.conversation.failed_reason = output.failed_reason.clone();
merge_output_metadata(&mut self.conversation, output);
self.persist().await;
output.conversation = Some(self.conversation._id);
}
pub(super) async fn record_failure(&mut self, reason: String) {
self.conversation.status = ConversationStatus::Failed;
self.conversation.failed_reason = Some(reason);
self.conversation.updated_at = unix_ms();
self.persist().await;
}
async fn persist(&self) {
match self.conversation.to_changes() {
Ok(changes) => {
if let Err(err) = self
.recorder
.conversations
.update_conversation(self.conversation._id, changes)
.await
{
log::warn!(
"failed to update subagent conversation {}: {err}",
self.conversation._id
);
}
}
Err(err) => {
log::warn!(
"failed to encode subagent conversation {} changes: {err}",
self.conversation._id
);
}
}
}
}
fn initial_request_messages(req: &CompletionRequest, timestamp: u64) -> Vec<Json> {
let mut content = req.content.clone();
if !req.prompt.is_empty() {
content.insert(0, req.prompt.clone().into());
}
if content.is_empty() {
Vec::new()
} else {
vec![json!(Message {
role: req.role.clone().unwrap_or_else(|| "user".to_string()),
content,
timestamp: Some(timestamp),
..Default::default()
})]
}
}
fn append_runner_history(
conversation: &mut Conversation,
chat_history: &[Message],
persisted_runner_history_len: &mut usize,
replace_existing: &mut bool,
) {
if chat_history.is_empty() {
return;
}
if *replace_existing {
conversation.messages.clear();
*replace_existing = false;
}
let incoming_len = chat_history.len();
let new_messages = if incoming_len >= *persisted_runner_history_len {
chat_history[*persisted_runner_history_len..].to_vec()
} else {
chat_history.to_vec()
};
conversation.append_messages(new_messages);
*persisted_runner_history_len = incoming_len;
}
fn merge_output_metadata(conversation: &mut Conversation, output: &AgentOutput) {
let mut extra = conversation.extra.take().unwrap_or_else(|| json!({}));
let Some(map) = extra.as_object_mut() else {
conversation.extra = Some(extra);
return;
};
if let Some(model) = &output.model {
map.insert("model".to_string(), model.clone().into());
}
if let Some(session) = &output.session {
map.insert("session".to_string(), session.clone().into());
}
if !output.tools_usage.is_empty() {
map.insert(
"tools_usage".to_string(),
serde_json::to_value(&output.tools_usage).unwrap_or_default(),
);
}
conversation.extra = Some(extra);
}