use std::path::Path;
use crate::agent_events::{AgentEvent, ToolCallStatus, ToolMutationStatus};
use crate::agent_sessions::event_facts as facts;
use crate::orchestration::AgentSessionReplayEvent;
use crate::value::VmError;
use harn_session_store::{ReadRange, SessionEventKind, SessionStore, StoreError, StoredEvent};
const RESTORE_PAGE: usize = 512;
pub async fn load_canonical_session_replay_events(
project_root: &Path,
session_id: &str,
) -> Result<Option<Vec<AgentSessionReplayEvent>>, VmError> {
let Some(store) = crate::stdlib::session_store::open_existing_canonical_store(project_root)?
else {
return Ok(None);
};
load_canonical_session_replay_events_from_store(&store, session_id).await
}
pub async fn load_canonical_session_replay_events_from_store(
store: &dyn SessionStore,
session_id: &str,
) -> Result<Option<Vec<AgentSessionReplayEvent>>, VmError> {
match store.describe(session_id).await {
Ok(_) => {}
Err(StoreError::NotFound(_)) => return Ok(None),
Err(error) => {
return Err(VmError::Runtime(format!(
"canonical session store describe {session_id}: {error}"
)))
}
}
let mut events = Vec::new();
let mut from = None;
loop {
let page = store
.read(
session_id,
ReadRange {
from_event_id: from,
limit: Some(RESTORE_PAGE),
..ReadRange::default()
},
)
.await
.map_err(|error| {
VmError::Runtime(format!(
"canonical session store read {session_id}: {error}"
))
})?;
for stored in page.events {
if let Some(event) = replay_event_from_stored(session_id, &stored) {
events.push(event);
}
}
match page.next_cursor {
Some(cursor) => from = Some(cursor),
None => break,
}
}
Ok(Some(close_unanswered_tool_calls(session_id, events)))
}
const UNANSWERED_TOOL_CALL_ERROR: &str = "No result: the session ended before this call finished.";
fn close_unanswered_tool_calls(
session_id: &str,
events: Vec<AgentSessionReplayEvent>,
) -> Vec<AgentSessionReplayEvent> {
let answered: std::collections::HashSet<String> = events
.iter()
.filter_map(|replayed| match &replayed.event {
AgentEvent::ToolCallUpdate {
tool_call_id,
status: ToolCallStatus::Completed | ToolCallStatus::Failed,
..
} => Some(tool_call_id.clone()),
_ => None,
})
.collect();
let mut last_call: std::collections::HashMap<String, (usize, String)> =
std::collections::HashMap::new();
for (index, replayed) in events.iter().enumerate() {
if let AgentEvent::ToolCall {
tool_call_id,
tool_name,
..
} = &replayed.event
{
if answered.contains(tool_call_id) {
continue;
}
let name = last_call
.get(tool_call_id)
.map(|(_, name)| name.clone())
.filter(|name| name != "tool")
.unwrap_or_else(|| tool_name.clone());
last_call.insert(tool_call_id.clone(), (index, name));
}
}
let mut closed = Vec::with_capacity(events.len() + last_call.len());
for (index, replayed) in events.into_iter().enumerate() {
let close = match &replayed.event {
AgentEvent::ToolCall { tool_call_id, .. } => last_call
.get(tool_call_id)
.filter(|(last, _)| *last == index)
.map(|(_, name)| (tool_call_id.clone(), name.clone())),
_ => None,
};
let (event_id, occurred_at_ms) = (replayed.event_id, replayed.occurred_at_ms);
closed.push(replayed);
if let Some((tool_call_id, tool_name)) = close {
closed.push(AgentSessionReplayEvent {
event_id,
kind: "tool_result".to_string(),
occurred_at_ms,
execution_id: None,
event: AgentEvent::ToolCallUpdate {
session_id: session_id.to_string(),
tool_call_id,
tool_name,
status: ToolCallStatus::Failed,
raw_output: Some(serde_json::Value::String(
UNANSWERED_TOOL_CALL_ERROR.to_string(),
)),
error: Some(UNANSWERED_TOOL_CALL_ERROR.to_string()),
duration_ms: None,
execution_duration_ms: None,
error_category: None,
mutation_status: ToolMutationStatus::Unknown,
changed_paths: None,
data: None,
executor: None,
parsing: None,
raw_input: None,
raw_input_partial: None,
audit: None,
},
});
}
}
closed
}
fn replay_event_from_stored(
session_id: &str,
stored: &StoredEvent,
) -> Option<AgentSessionReplayEvent> {
let transcript = stored.payload.get("transcript_event")?;
let kind = transcript.get("kind").and_then(serde_json::Value::as_str);
if matches!(kind, Some("turn_phase_changed" | "agent_run_terminal")) {
let metadata = transcript.get("metadata")?;
let event = if kind == Some("agent_run_terminal") {
AgentEvent::TurnPhaseChanged {
session_id: session_id.to_string(),
phase: crate::agent_events::AgentTurnPhase::from_terminal_record(metadata)?,
}
} else {
AgentEvent::from_host_payload(session_id, "turn_phase_changed", metadata).ok()??
};
if kind == Some("turn_phase_changed")
&& matches!(
event,
AgentEvent::TurnPhaseChanged {
phase: crate::agent_events::AgentTurnPhase::Terminal { .. },
..
}
)
{
return None;
}
return Some(AgentSessionReplayEvent {
event_id: stored.event_id,
kind: stored_kind_label(&stored.kind),
occurred_at_ms: stored.ts_ms,
execution_id: None,
event,
});
}
if matches!(stored.kind, SessionEventKind::Message)
&& transcript
.get("visibility")
.and_then(serde_json::Value::as_str)
== Some("internal")
{
return None;
}
let raw_message = stored.payload.get("raw_message");
let role = transcript
.get("role")
.and_then(serde_json::Value::as_str)
.unwrap_or_default();
let text = transcript
.get("text")
.and_then(serde_json::Value::as_str)
.unwrap_or_default();
let event = match (&stored.kind, role) {
(SessionEventKind::Message, "user") => AgentEvent::UserMessage {
session_id: session_id.to_string(),
message_id: transcript
.get("id")
.and_then(serde_json::Value::as_str)
.unwrap_or(&stored.record_hash)
.to_string(),
content: user_content_blocks(transcript, text),
},
(SessionEventKind::Message, _) if !text.is_empty() => AgentEvent::AgentMessageChunk {
session_id: session_id.to_string(),
content: text.to_string(),
},
(SessionEventKind::ToolCall, _) => {
let tool_call_id = tool_call_id(stored, transcript)?;
let provider_call = provider_tool_call(raw_message, &tool_call_id);
AgentEvent::ToolCall {
session_id: session_id.to_string(),
tool_name: tool_name(transcript, raw_message, provider_call),
tool_call_id,
kind: None,
status: ToolCallStatus::Completed,
raw_input: transcript
.get("input")
.or_else(|| transcript.pointer("/metadata/raw_input"))
.or_else(|| provider_call.and_then(|call| call.get("arguments")))
.cloned()
.unwrap_or(serde_json::Value::Null),
parsing: None,
audit: None,
}
}
(SessionEventKind::ToolResult, _) => {
let failed = facts::bool_at_any(&stored.payload, &facts::TOOL_IS_ERROR_ANY);
let data = stored
.payload
.pointer(facts::TOOL_RESULT_DATA)
.filter(|data| data.is_object());
AgentEvent::ToolCallUpdate {
session_id: session_id.to_string(),
tool_call_id: tool_call_id(stored, transcript)?,
tool_name: tool_name(transcript, raw_message, None),
status: if failed {
ToolCallStatus::Failed
} else {
ToolCallStatus::Completed
},
raw_output: Some(serde_json::Value::String(text.to_string())),
error: failed.then(|| text.to_string()),
duration_ms: None,
execution_duration_ms: None,
error_category: None,
mutation_status: mutation_status(data),
changed_paths: changed_paths(data),
data: data.cloned(),
executor: None,
parsing: None,
raw_input: None,
raw_input_partial: None,
audit: None,
}
}
_ => return None,
};
Some(AgentSessionReplayEvent {
event_id: stored.event_id,
kind: stored_kind_label(&stored.kind),
occurred_at_ms: stored.ts_ms,
execution_id: None,
event,
})
}
fn user_content_blocks(transcript: &serde_json::Value, text: &str) -> Vec<serde_json::Value> {
match transcript
.get("blocks")
.and_then(serde_json::Value::as_array)
{
Some(blocks) if !blocks.is_empty() => blocks.clone(),
_ => vec![serde_json::json!({"type": "text", "text": text})],
}
}
fn tool_call_id(stored: &StoredEvent, transcript: &serde_json::Value) -> Option<String> {
transcript
.get("tool_call_id")
.and_then(serde_json::Value::as_str)
.map(str::to_string)
.or_else(|| {
stored
.headers
.get("tool_call_id")
.filter(|value| !value.is_empty())
.cloned()
})
}
fn provider_tool_call<'a>(
raw_message: Option<&'a serde_json::Value>,
tool_call_id: &str,
) -> Option<&'a serde_json::Value> {
raw_message?
.get("tool_calls")?
.as_array()?
.iter()
.find(|call| call.get("id").and_then(serde_json::Value::as_str) == Some(tool_call_id))
}
fn tool_name(
transcript: &serde_json::Value,
raw_message: Option<&serde_json::Value>,
provider_call: Option<&serde_json::Value>,
) -> String {
[
Some(transcript),
transcript.get("metadata"),
raw_message,
provider_call,
provider_call.and_then(|call| call.get("function")),
]
.into_iter()
.flatten()
.find_map(|value| {
value
.get("tool_name")
.or_else(|| value.get("name"))
.and_then(serde_json::Value::as_str)
.filter(|name| !name.trim().is_empty())
})
.unwrap_or("tool")
.to_string()
}
fn mutation_status(data: Option<&serde_json::Value>) -> ToolMutationStatus {
let declared = data
.and_then(|data| data.get("mutation_status"))
.and_then(serde_json::Value::as_str);
ToolMutationStatus::ALL
.into_iter()
.find(|status| Some(status.as_str()) == declared)
.unwrap_or(ToolMutationStatus::Unknown)
}
fn changed_paths(data: Option<&serde_json::Value>) -> Option<Vec<String>> {
let paths = data?.get("changed_paths")?.as_array()?;
Some(
paths
.iter()
.filter_map(serde_json::Value::as_str)
.filter(|path| !path.trim().is_empty())
.map(str::to_string)
.collect(),
)
}
fn stored_kind_label(kind: &SessionEventKind) -> String {
match kind {
SessionEventKind::Custom { custom_type } => custom_type.clone(),
other => serde_json::to_value(other)
.ok()
.and_then(|value| value.as_str().map(str::to_string))
.unwrap_or_else(|| "message".to_string()),
}
}
#[cfg(test)]
mod tests;