use super::{bound_card, initial_card, omit, project_detail};
use crate::{
output::ActivityStatus,
sessions::{
SessionEvent, SessionEventKind,
chat::{self, ChatMessage},
},
};
use serde_json::{Value, json};
use std::collections::HashMap;
fn subagent_tasks(event: &SessionEvent) -> Option<crate::subagents::SubagentsArgs> {
if event.payload["name"] != "subagents" {
return None;
}
crate::subagents::SubagentsArgs::from_recorded_call(event.payload["arguments"].clone())
}
pub(in crate::local_agent) fn expand_tasks(
events: &[SessionEvent],
messages: Vec<ChatMessage>,
) -> Result<Vec<ChatMessage>, &'static str> {
let mut expanded = Vec::with_capacity(messages.len());
let mut text_bytes: usize = messages.iter().map(|message| message.text.len()).sum();
for message in messages {
let source = message
.activity
.as_ref()
.filter(|source| source.task_index.is_none());
let tasks = source
.and_then(|source| source.call)
.and_then(|index| subagent_tasks(&events[index]));
let source = source.cloned();
if expanded.len() == chat::CHAT_MAX_ENTRIES {
return Err("history_limit");
}
expanded.push(message);
if let (Some(args), Some(source)) = (tasks, source) {
for (index, task) in args.tasks.iter().enumerate() {
let mut activity = source.clone();
activity.task_index = Some(index);
let text = crate::output::sanitize_display_text(&task.intent);
text_bytes = text_bytes.saturating_add(text.len());
if text_bytes > chat::CHAT_MAX_BYTES {
return Err("history_limit");
}
expanded.push(ChatMessage {
kind: "activity",
text,
partial: false,
activity: Some(activity),
});
if expanded.len() > chat::CHAT_MAX_ENTRIES {
return Err("history_limit");
}
}
}
}
Ok(expanded)
}
pub(in crate::local_agent) fn project(
events: &[SessionEvent],
messages: &[ChatMessage],
ids: &[String],
budget: usize,
) -> Result<Vec<Option<Value>>, &'static str> {
let previews = crate::sessions::tool_display::RecordedPreviews::new(events);
let assessments = crate::protection::prompt_injection::RecordedAssessments::new(events);
let execution_summaries = crate::code_mode::audit::recorded_summaries(events);
let mut calls = HashMap::<(usize, &str), Option<usize>>::new();
let mut by_event = HashMap::new();
for (index, message) in messages.iter().enumerate() {
if let Some(source) = &message.activity
&& source.task_index.is_none()
&& let Some(call_index) = source.call
&& let Some(id) = events[call_index].payload["id"].as_str()
{
by_event.insert(call_index, index);
if events[call_index].kind() == Some(SessionEventKind::ToolCall)
&& events[call_index].payload["name"] == crate::code_mode::TOOL_NAME
{
calls
.entry((source.turn, id))
.and_modify(|value| *value = None)
.or_insert(Some(index));
}
}
}
let mut cards = Vec::with_capacity(messages.len());
let mut retained_bytes = 0;
let mut parents = vec![None; messages.len()];
for (index, message) in messages.iter().enumerate() {
let Some(source) = &message.activity else {
cards.push(None);
continue;
};
let call = source
.call
.and_then(|index| chat::tool_call(&events[index]))
.or_else(|| {
source.result.and_then(|index| {
chat::tool_result(&events[index], &HashMap::new())
.ok()
.map(|(call, _)| call)
})
});
let mut card = initial_card(&ids[index], source.task_index.is_some());
card["origin"] = json!("saved");
if let Some(task_index) = source.task_index {
card["omissions"] = json!([]);
let args = source.call.and_then(|index| subagent_tasks(&events[index]));
let task = args.as_ref().and_then(|args| args.tasks.get(task_index));
let report = source.result.and_then(|index| {
crate::output::tool_card::TaskBatchTelemetry::parse_for_tasks(
&events[index].payload["result"]["metadata"]["subagent_telemetry"],
&events[source.call?].payload["arguments"],
)
});
let recorded_task = report
.as_ref()
.and_then(|report| report.tasks.get(task_index));
if let Some(task) = task {
card["title"] = json!(crate::output::sanitize_display_text(&task.intent));
}
card["task_id"] = json!(ids[index]);
parents[index] = source.call.and_then(|call| by_event.get(&call).copied());
let mut legacy = crate::output::tool_card::TaskTelemetry::new(task_index);
if let Some(task) = task {
legacy.set_intent(&task.intent);
let mut metadata = crate::output::ActivityMetadata::new("");
if let Some(profile) = &task.identity {
metadata.fields.push(("identity".into(), profile.clone()));
}
if let Some(agent) = &task.agent {
metadata.fields.push(("agent".into(), agent.clone()));
}
legacy.metadata(&metadata);
}
if let Some(result_index) = source.result {
let content = events[result_index].payload["result"]["content"]
.as_str()
.filter(|text| text.len() <= 4 * 1024 * 1024);
if let Some(output) = content
.and_then(|text| {
serde_json::from_str::<crate::subagents::SubagentsOutput>(text).ok()
})
.filter(|output| output.results.len() <= crate::subagents::MAX_SUBAGENT_TASKS)
{
let task_id = format!("g{}", task_index + 1);
let matches: Vec<_> = output
.results
.iter()
.filter(|task| task.id == task_id)
.collect();
if let [result] = matches.as_slice() {
card["outcome"] = json!(match result.status {
crate::subagents::SubagentStatus::Completed => "success",
crate::subagents::SubagentStatus::Failed => "failure",
});
legacy.outcome = card["outcome"].as_str().map(str::to_owned);
legacy.usage = result.usage;
legacy.changed_paths = Some(result.changed_files.len() as u64);
if !result.changed_files.is_empty() {
card["changes"] = json!(if result.status
== crate::subagents::SubagentStatus::Completed
{
"applied"
} else {
"partial"
});
}
}
}
}
let selected = recorded_task.unwrap_or(&legacy);
if let Some(outcome) = &selected.outcome {
card["outcome"] = json!(outcome);
}
super::project_task_telemetry(
&mut card,
selected,
if recorded_task.is_some() {
"recorded"
} else {
"not_recorded"
},
);
if recorded_task.is_none() {
omit(&mut card, "rows", "not_recorded", None);
}
} else if let Some(call) = call {
let detail = if let Some(result_index) = source.result {
let event = &events[result_index];
let mut matched_calls = HashMap::new();
matched_calls.insert(call.id.clone(), call.clone());
chat::tool_result(event, &matched_calls)
.ok()
.map(|(_, mut result)| {
previews.apply(result_index, event, &mut result);
let assessment = assessments.for_result(result_index);
if !assessment.is_null() {
result.metadata["prompt_injection_protection"] = assessment.clone();
}
let label = crate::output::tool_display_label(&call);
let mut detail = crate::output::tool_display::tool_activity_detail(
&call,
&result,
label,
if result.success {
ActivityStatus::Success
} else {
ActivityStatus::Failed
},
);
crate::output::tool_card::ResourceReport::restore(
&mut detail,
&event.payload,
);
if matches!(call.name.as_str(), "code_mode" | "subagents") {
detail.metadata["telemetry_persistence"] = json!("recorded");
}
detail
})
} else {
let label = crate::output::tool_display_label(&call);
Some(crate::output::tool_display::tool_activity_detail_pending(
&call,
label,
ActivityStatus::Running,
))
};
if let Some(mut detail) = detail {
if source.result.is_none()
&& let Some(summary) = source
.call
.and_then(|index| execution_summaries.get(&index))
{
detail.metadata["code_mode_execution"] = summary.clone();
detail.metadata["telemetry_persistence"] = json!("recorded");
}
project_detail(
&mut card,
&detail,
true,
&mut super::BashNoticeState::default(),
);
}
if let Some(call_index) = source.call {
let event = &events[call_index];
if event.kind() == Some(SessionEventKind::CodeModeToolCall) {
parents[index] = event.payload["parent_call_id"]
.as_str()
.and_then(|parent| calls.get(&(source.turn, parent)).copied().flatten())
.filter(|&parent| {
messages[parent]
.activity
.as_ref()
.and_then(|source| source.call)
.is_some_and(|parent_call| parent_call < call_index)
});
if parents[index].is_none() {
card["parent_state"] = json!("missing");
omit(&mut card, "ancestry", "missing_source", None);
}
}
}
if source.call.is_none() {
card["parent_state"] = json!("missing");
omit(&mut card, "ancestry", "missing_source", None);
omit(&mut card, "metadata", "not_recorded", None);
}
} else {
omit(&mut card, "blocks", "missing_source", None);
card["parent_state"] = json!("missing");
omit(&mut card, "ancestry", "missing_source", None);
}
card["origin"] = json!("saved");
card["state"] = json!("finished");
card["revision"] = json!(0);
if card["outcome"].is_null() {
card["outcome"] = json!(if source.interrupted {
"interrupted"
} else {
"unknown"
});
}
bound_card(&mut card);
retained_bytes += super::bytes(&card);
cards.push(Some(card));
limit_retention(&mut cards, &mut retained_bytes, budget)?;
}
for index in 0..parents.len() {
let mut path = vec![index];
let mut cursor = index;
while let Some(parent) = parents[cursor] {
if path.contains(&parent) || path.len() > 8 {
parents[index] = None;
if let Some(card) = &mut cards[index] {
let previous = super::bytes(card);
card["parent_state"] = json!("omitted");
omit(card, "ancestry", "size_limit", None);
retained_bytes = retained_bytes - previous + super::bytes(card);
}
limit_retention(&mut cards, &mut retained_bytes, budget)?;
break;
}
path.push(parent);
cursor = parent;
}
}
let mut children = vec![Vec::new(); cards.len()];
for (index, parent) in parents.iter().enumerate() {
if let Some(parent) = parent {
children[*parent].push(index);
}
}
for index in 0..cards.len() {
let rows: Vec<_> = children[index].iter().take(32).filter_map(|&child| cards[child].as_ref().map(|card| {
json!({"activity_id":ids[child],"task_id":card["task_id"],"title":card["title"],"state":"finished","outcome":card["outcome"],"changes":card["changes"]})
})).collect();
let task_children: Vec<_> = children[index]
.iter()
.copied()
.filter(|&child| {
messages[child]
.activity
.as_ref()
.is_some_and(|source| source.task_index.is_some())
})
.collect();
let finished_tasks = task_children
.iter()
.filter(|&&child| {
cards[child].as_ref().is_some_and(|card| {
matches!(
card["outcome"].as_str(),
Some("success" | "failure" | "cancelled")
)
})
})
.count();
let Some(card) = &mut cards[index] else {
continue;
};
let previous = super::bytes(card);
if let Some(parent) = parents[index] {
card["parent_activity_id"] = json!(ids[parent]);
card["parent_state"] = json!("available");
}
if !rows.is_empty() {
if card["blocks"].as_array().unwrap().len() == 16 {
card["blocks"].as_array_mut().unwrap().pop();
omit(card, "blocks", "size_limit", None);
}
card["blocks"]
.as_array_mut()
.unwrap()
.push(json!({"type":"children","children":rows,"total":children[index].len()}));
if children[index].len() > 32 {
omit(
card,
"children",
"size_limit",
Some(children[index].len() - 32),
);
}
}
if !task_children.is_empty() {
if card["blocks"].as_array().unwrap().len() == 16 {
card["blocks"].as_array_mut().unwrap().pop();
omit(card, "blocks", "size_limit", None);
}
card["blocks"].as_array_mut().unwrap().push(json!({"type":"progress","label":"Tasks finished","completed":finished_tasks,"total":task_children.len(),"unit":"items"}));
}
bound_card(card);
retained_bytes = retained_bytes - previous + super::bytes(card);
limit_retention(&mut cards, &mut retained_bytes, budget)?;
}
Ok(cards)
}
fn limit_retention(
cards: &mut [Option<Value>],
retained: &mut usize,
budget: usize,
) -> Result<(), &'static str> {
if *retained <= budget {
return Ok(());
}
for card in cards.iter_mut().rev().flatten() {
let previous = super::bytes(card);
super::make_core(card, "budget_limit");
*retained = *retained - previous + super::bytes(card);
if *retained <= budget {
return Ok(());
}
}
Err("history_limit")
}