use std::collections::HashMap;
use serde_json::{Value, json};
use super::metadata::{TimelineSemanticInfo, TimelineSummaryInfo};
use crate::bridge_protocol::TimelineEntry;
pub(super) fn finalize_timeline_entries(entries: &mut [TimelineEntry]) {
let mut semantic_by_call: HashMap<
(String, String, String),
(TimelineSemanticInfo, TimelineSummaryInfo),
> = HashMap::new();
for entry in entries.iter() {
let Some(call_id) = metadata_call_id(&entry.metadata) else {
continue;
};
let Some(semantic) = metadata_semantic(&entry.metadata) else {
continue;
};
if semantic.role == "output" {
continue;
}
let summary = metadata_summary(&entry.metadata).unwrap_or_default();
semantic_by_call.insert(
(
entry.thread_id.clone(),
turn_lookup_key(entry.turn_id.as_deref()),
call_id,
),
(semantic, summary),
);
}
for entry in entries.iter_mut() {
if !is_output_entry_type(&entry.entry_type) {
continue;
}
if metadata_semantic(&entry.metadata).is_some() {
continue;
}
let Some(call_id) = metadata_call_id(&entry.metadata) else {
continue;
};
let lookup_key = (
entry.thread_id.clone(),
turn_lookup_key(entry.turn_id.as_deref()),
call_id,
);
let Some((semantic, summary)) = semantic_by_call.get(&lookup_key) else {
continue;
};
let mut inherited_semantic = semantic.clone();
inherited_semantic.role = "output".to_string();
set_metadata_semantic(entry, &inherited_semantic, Some(summary));
}
}
pub(super) fn merge_timeline_metadata(existing: &mut Value, incoming: Value) {
if existing.is_null() || !existing.is_object() || !incoming.is_object() {
*existing = incoming;
return;
}
let Some(existing_object) = existing.as_object_mut() else {
*existing = incoming;
return;
};
let Some(incoming_object) = incoming.as_object() else {
*existing = incoming;
return;
};
for (key, value) in incoming_object {
match key.as_str() {
"payload" => merge_timeline_payload(existing_object, value),
"stream" | "lifecycle" | "semantic" | "summary" => {
merge_timeline_object(existing_object, key, value)
}
_ => {
existing_object.insert(key.clone(), value.clone());
}
}
}
}
fn is_output_entry_type(entry_type: &str) -> bool {
matches!(
entry_type,
"function_call_output"
| "custom_tool_call_output"
| "shell_call_output"
| "local_shell_call_output"
| "apply_patch_call_output"
| "computer_call_output"
)
}
fn turn_lookup_key(turn_id: Option<&str>) -> String {
turn_id.unwrap_or_default().to_string()
}
fn metadata_call_id(metadata: &Value) -> Option<String> {
metadata
.get("payload")
.and_then(|payload| payload.get("callId"))
.and_then(Value::as_str)
.map(ToOwned::to_owned)
}
fn metadata_semantic(metadata: &Value) -> Option<TimelineSemanticInfo> {
TimelineSemanticInfo::from_value(metadata.get("semantic")?)
}
fn metadata_summary(metadata: &Value) -> Option<TimelineSummaryInfo> {
TimelineSummaryInfo::from_value(metadata.get("summary")?)
}
fn set_metadata_semantic(
entry: &mut TimelineEntry,
semantic: &TimelineSemanticInfo,
summary: Option<&TimelineSummaryInfo>,
) {
if entry.metadata.is_null() || !entry.metadata.is_object() {
entry.metadata = json!({});
}
let Some(metadata_object) = entry.metadata.as_object_mut() else {
return;
};
metadata_object.insert("semantic".to_string(), semantic.to_value());
if let Some(summary) = summary {
metadata_object.insert("summary".to_string(), summary.to_value());
}
}
fn merge_timeline_payload(existing_object: &mut serde_json::Map<String, Value>, incoming: &Value) {
let payload = existing_object
.entry("payload".to_string())
.or_insert_with(|| json!({}));
if payload.is_null() || !payload.is_object() || !incoming.is_object() {
*payload = incoming.clone();
return;
}
let Some(existing_payload) = payload.as_object_mut() else {
*payload = incoming.clone();
return;
};
let Some(incoming_payload) = incoming.as_object() else {
*payload = incoming.clone();
return;
};
for (key, value) in incoming_payload {
if matches!(key.as_str(), "summary" | "content")
&& value.as_array().is_some_and(|array| array.is_empty())
&& existing_payload
.get(key)
.and_then(Value::as_array)
.is_some_and(|array| !array.is_empty())
{
continue;
}
existing_payload.insert(key.clone(), value.clone());
}
}
fn merge_timeline_object(
existing_object: &mut serde_json::Map<String, Value>,
key: &str,
incoming: &Value,
) {
if incoming.is_null() {
return;
}
let target = existing_object
.entry(key.to_string())
.or_insert_with(|| json!({}));
if target.is_null() || !target.is_object() || !incoming.is_object() {
*target = incoming.clone();
return;
}
let Some(target_object) = target.as_object_mut() else {
*target = incoming.clone();
return;
};
let Some(incoming_object) = incoming.as_object() else {
*target = incoming.clone();
return;
};
for (child_key, child_value) in incoming_object {
target_object.insert(child_key.clone(), child_value.clone());
}
}