use anyhow::Result;
use serde_json::{Value, json};
use super::super::helpers::{optional_string, required_string};
use super::content;
use super::content::{format_plan_payload, timeline_entry_status};
use super::metadata::{
timeline_collapse_hint, timeline_has_visible_content, timeline_lifecycle_info,
timeline_metadata, timeline_render_kind, timeline_stream_metadata,
};
use super::metadata_merge::finalize_timeline_entries;
use super::normalize::{canonical_timeline_entry_type, timeline_entry_id};
use super::payload::timeline_payload_from_thread_item;
use super::semantic::timeline_semantic_from_item;
use super::summary::timeline_summary_from_item;
use crate::bridge_protocol::TimelineEntry;
pub(super) fn timeline_entries_from_thread(
runtime_id: &str,
thread: &Value,
) -> Result<Vec<TimelineEntry>> {
let thread_id = required_string(thread, "id")?.to_string();
let mut entries = Vec::new();
for turn in thread
.get("turns")
.and_then(Value::as_array)
.into_iter()
.flatten()
{
let turn_id = optional_string(turn, "id");
for item in turn
.get("items")
.and_then(Value::as_array)
.into_iter()
.flatten()
{
if let Some(entry) = timeline_entry_from_thread_item(
runtime_id,
&thread_id,
turn_id.as_deref(),
item,
"thread_item",
false,
true,
) {
entries.push(entry);
}
}
}
finalize_timeline_entries(&mut entries);
Ok(entries)
}
pub(super) fn timeline_entry_from_thread_item(
runtime_id: &str,
thread_id: &str,
turn_id: Option<&str>,
item: &Value,
source_kind: &str,
is_streaming: bool,
authoritative: bool,
) -> Option<TimelineEntry> {
let item_id = optional_string(item, "id");
let raw_type = required_string(item, "type").ok()?.to_string();
let entry_type = canonical_timeline_entry_type(&raw_type, item);
let render_kind = timeline_render_kind(&entry_type);
let stream_phase = optional_string(item, "phase");
let payload = timeline_payload_from_thread_item(&entry_type, item);
let semantic = timeline_semantic_from_item(&raw_type, &entry_type, item, &payload);
let summary = timeline_summary_from_item(&entry_type, item, &payload, semantic.as_ref());
let text = content::timeline_text_from_thread_item(&entry_type, item);
let entry_status = timeline_entry_status(&entry_type, item, is_streaming);
let lifecycle = timeline_lifecycle_info(
authoritative,
entry_status.as_deref(),
timeline_has_visible_content(&text, &payload, summary.as_ref()),
);
let metadata = timeline_metadata(
source_kind,
&raw_type,
render_kind,
timeline_collapse_hint(render_kind, &entry_type),
timeline_stream_metadata(
is_streaming,
authoritative,
stream_phase.clone(),
None,
None,
),
Some(&lifecycle),
payload,
semantic.as_ref(),
summary.as_ref(),
item.clone(),
);
Some(TimelineEntry {
id: timeline_entry_id(turn_id, item_id.as_deref(), &entry_type),
runtime_id: runtime_id.to_string(),
thread_id: thread_id.to_string(),
turn_id: turn_id.map(ToOwned::to_owned),
item_id,
entry_type: entry_type.clone(),
title: content::timeline_entry_title(&entry_type, item),
text,
status: entry_status,
metadata,
})
}
pub(super) fn timeline_entry_from_plan_update(
runtime_id: &str,
thread_id: &str,
turn_id: &str,
explanation: Option<String>,
plan: Value,
) -> TimelineEntry {
let item_id = format!("turn-plan:{turn_id}");
TimelineEntry {
id: timeline_entry_id(Some(turn_id), Some(&item_id), "plan"),
runtime_id: runtime_id.to_string(),
thread_id: thread_id.to_string(),
turn_id: Some(turn_id.to_string()),
item_id: Some(item_id),
entry_type: "plan".to_string(),
title: Some("执行计划".to_string()),
text: format_plan_payload(explanation.as_deref(), plan.as_array()),
status: Some("inProgress".to_string()),
metadata: timeline_metadata(
"stream_event",
"turn/plan/updated",
"plan",
timeline_collapse_hint("plan", "plan"),
timeline_stream_metadata(true, false, None, None, None),
None,
json!({
"explanation": explanation,
"plan": plan,
}),
None,
None,
json!({
"explanation": explanation,
"plan": plan,
}),
),
}
}