use super::*;
pub(super) const MAX_SUBAGENT_REPLAY_JSON_BYTES: usize = 4 * 1024 * 1024;
pub(super) const HYDRATED_SUBAGENT_DEPTH: usize = 1;
pub(super) fn hydrate_subagent_tool_call(
ui_state: &mut state::MissionControlState,
parent_id: &ActivityId,
call: &ToolCall,
) {
if call.name != tool_name::SUBAGENTS {
return;
}
let Some(arguments) = parse_bounded_subagents_args(&call.arguments) else {
return;
};
for (index, task) in arguments.tasks.iter().enumerate() {
let task_id = format!("g{}", index + 1);
ui_state.apply_historical_activity_event(ActivityEvent::Started {
id: ActivityId::new(format!("{}/{}", parent_id.as_str(), task_id)),
parent_id: Some(parent_id.clone()),
kind: ActivityKind::SubagentTask,
status: ActivityStatus::Running,
metadata: hydrated_subagent_task_metadata(&task_id, task),
});
}
}
pub(super) fn hydrate_subagent_tool_result(
ui_state: &mut state::MissionControlState,
parent_id: &ActivityId,
call: &ToolCall,
result: &ToolResult,
) {
let output = parse_bounded_subagents_output(&result.content);
if let Some(output) = output.as_ref() {
ensure_hydrated_subagent_parent(ui_state, parent_id, call);
for task_result in &output.results {
let Some(task_activity_id) =
hydrated_subagent_task_activity_id(parent_id, &task_result.id)
else {
continue;
};
if !ui_state.nodes.contains_key(&task_activity_id) {
ui_state.apply_historical_activity_event(ActivityEvent::Started {
id: task_activity_id,
parent_id: Some(parent_id.clone()),
kind: ActivityKind::SubagentTask,
status: ActivityStatus::Running,
metadata: hydrated_subagent_result_metadata(task_result),
});
}
}
}
let task_ids = subagent_task_children(ui_state, parent_id);
if task_ids.is_empty() {
return;
}
let fallback_status = if result.success {
ActivityStatus::Success
} else {
ActivityStatus::Failed
};
let mut reconciled = std::collections::HashSet::new();
if let Some(output) = output {
for task_result in &output.results {
let Some(task_activity_id) =
hydrated_subagent_task_activity_id(parent_id, &task_result.id)
else {
continue;
};
if !task_ids.contains(&task_activity_id) || !reconciled.insert(task_activity_id.clone())
{
continue;
}
finish_hydrated_subagent_task(
ui_state,
task_activity_id,
Some(task_result),
fallback_status,
);
}
}
for task_activity_id in task_ids {
if !reconciled.contains(&task_activity_id) {
finish_hydrated_subagent_task(ui_state, task_activity_id, None, fallback_status);
}
}
}
fn ensure_hydrated_subagent_parent(
ui_state: &mut state::MissionControlState,
parent_id: &ActivityId,
call: &ToolCall,
) {
if ui_state.nodes.contains_key(parent_id) {
return;
}
ui_state.apply_historical_activity_event(ActivityEvent::Started {
id: parent_id.clone(),
parent_id: None,
kind: ActivityKind::Tool,
status: crate::output::pending_activity_status(call),
metadata: ActivityMetadata::new(crate::output::tool_display_label(call)),
});
}
fn subagent_task_children(
ui_state: &state::MissionControlState,
parent_id: &ActivityId,
) -> Vec<ActivityId> {
ui_state
.children
.get(parent_id)
.cloned()
.unwrap_or_default()
.into_iter()
.filter(|id| {
ui_state
.nodes
.get(id)
.is_some_and(|node| node.kind == ActivityKind::SubagentTask)
})
.collect()
}
pub(super) fn finish_outstanding_subagent_tasks(
ui_state: &mut state::MissionControlState,
parent_id: &ActivityId,
status: ActivityStatus,
) {
for id in subagent_task_children(ui_state, parent_id) {
let outstanding = ui_state.nodes.get(&id).is_some_and(|node| {
matches!(
node.status,
ActivityStatus::Queued | ActivityStatus::Running | ActivityStatus::Writing
)
});
if outstanding {
ui_state.apply_historical_activity_event(ActivityEvent::Finished {
id,
status,
metadata: None,
});
}
}
}
fn finish_hydrated_subagent_task(
ui_state: &mut state::MissionControlState,
id: ActivityId,
result: Option<&SubagentTaskResult>,
fallback_status: ActivityStatus,
) {
let status = result
.map(subagent_result_activity_status)
.unwrap_or(fallback_status);
if let Some(result) = result {
if let Some(usage) = result.usage {
let request_sequence = usage.latest_request_sequence.unwrap_or_default();
let final_usage = usage.latest_final;
if !ui_state.session_usage.has_recorded_activity(id.as_str()) {
ui_state
.session_usage
.observe(crate::tui::session_usage::SessionUsageRecord {
id: format!("legacy/activity/{}", id.as_str()),
usage: usage.whole_run,
request_sequence,
final_usage,
});
}
ui_state.apply_historical_activity_event(ActivityEvent::UsageSnapshot {
id: id.clone(),
usage,
request_sequence,
final_usage,
});
} else if !ui_state.session_usage.has_recorded_activity(id.as_str()) {
ui_state.session_usage.incomplete = true;
}
ui_state.apply_historical_activity_event(ActivityEvent::FinalPreview {
id: id.clone(),
preview: subagent_result_preview(result),
metadata: None,
status: Some(status),
});
}
ui_state.apply_historical_activity_event(ActivityEvent::Finished {
id,
status,
metadata: None,
});
}
fn parse_bounded_subagents_args(arguments: &serde_json::Value) -> Option<SubagentsArgs> {
let serialized = serde_json::to_vec(arguments).ok()?;
if serialized.len() > MAX_SUBAGENT_REPLAY_JSON_BYTES {
return None;
}
let arguments: SubagentsArgs = serde_json::from_slice(&serialized).ok()?;
arguments.validated_concurrency().ok()?;
Some(arguments)
}
fn parse_bounded_subagents_output(content: &str) -> Option<SubagentsOutput> {
if content.len() > MAX_SUBAGENT_REPLAY_JSON_BYTES {
return None;
}
let output: SubagentsOutput = serde_json::from_str(content).ok()?;
(output.results.len() <= MAX_SUBAGENT_TASKS).then_some(output)
}
fn hydrated_subagent_task_metadata(task_id: &str, task: &SubagentTask) -> ActivityMetadata {
hydrated_subagent_metadata(
task_id,
&task.intent,
task.identity.as_deref(),
task.agent.as_deref(),
)
}
fn hydrated_subagent_result_metadata(result: &SubagentTaskResult) -> ActivityMetadata {
hydrated_subagent_metadata(
&result.id,
&result.intent,
result.identity.as_deref(),
result.agent.as_deref(),
)
}
fn hydrated_subagent_metadata(
task_id: &str,
intent: &str,
identity: Option<&str>,
agent: Option<&str>,
) -> ActivityMetadata {
let intent = bounded_subagent_metadata_value(intent);
let intent = if intent.is_empty() {
"task"
} else {
intent.as_str()
};
let label = format!("{task_id} · depth {HYDRATED_SUBAGENT_DEPTH} · {intent}");
let mut metadata = ActivityMetadata::new(bounded_subagent_metadata_value(&label));
metadata
.fields
.push(("depth".to_string(), HYDRATED_SUBAGENT_DEPTH.to_string()));
if let Some(identity) = identity {
let identity = bounded_subagent_metadata_value(identity);
if !identity.is_empty() {
metadata.fields.push(("identity".to_string(), identity));
}
}
if let Some(agent) = agent {
let agent = bounded_subagent_metadata_value(agent);
if !agent.is_empty() {
metadata.fields.push(("agent".to_string(), agent));
}
}
metadata
}
fn bounded_subagent_metadata_value(value: &str) -> String {
crate::tui::activity::sanitize_and_bound_preview(value)
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
}
fn hydrated_subagent_task_activity_id(parent_id: &ActivityId, task_id: &str) -> Option<ActivityId> {
let digits = task_id.strip_prefix('g')?;
if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
let index = digits.parse::<usize>().ok()?;
if !(1..=MAX_SUBAGENT_TASKS).contains(&index) || format!("g{index}") != task_id {
return None;
}
Some(ActivityId::new(format!(
"{}/{}",
parent_id.as_str(),
task_id
)))
}
fn subagent_result_activity_status(result: &SubagentTaskResult) -> ActivityStatus {
match &result.status {
SubagentStatus::Completed => ActivityStatus::Success,
SubagentStatus::Failed => ActivityStatus::Failed,
}
}
fn subagent_result_preview(result: &SubagentTaskResult) -> String {
let preview = match &result.status {
SubagentStatus::Completed => result.output.as_str(),
SubagentStatus::Failed => result
.error
.as_deref()
.filter(|error| !error.trim().is_empty())
.unwrap_or(result.output.as_str()),
};
crate::tui::activity::sanitize_and_bound_preview(preview)
}