use mentra::{
SessionEvent,
agent::{
ElidedToolResult as MentraElidedToolResult,
RequestToolResultElisionPolicy as MentraRequestToolResultElisionPolicy,
ToolResultContentKind as MentraToolResultContentKind,
ToolResultElisionAction as MentraToolResultElisionAction,
},
session::{
NoticeSeverity as MentraNoticeSeverity, PermissionOutcome as MentraPermissionOutcome,
PermissionRuleScope, TaskKind as MentraTaskKind, TaskLifecycleStatus, ToolMutability,
},
};
use serde_json::Value;
use super::{
ElidedToolResult, Event, Mutability, NoticeSeverity, PermissionOutcome,
RequestToolResultElisionPolicy, RuleScope, TaskKind, TaskStatus, ToolResultContentKind,
ToolResultElisionAction,
};
pub(super) fn from_session_event(event: &SessionEvent) -> Option<Event> {
let mapped = match event {
SessionEvent::SessionStarted { .. } => return None,
SessionEvent::UserMessage { text, image_count } => Event::UserMessage {
text: text.clone(),
image_count: *image_count,
},
SessionEvent::AssistantTokenDelta { delta, .. } => Event::AssistantDelta {
text: delta.clone(),
},
SessionEvent::AssistantReasoningDelta { delta, .. } => Event::AssistantReasoningDelta {
text: delta.clone(),
},
SessionEvent::AssistantMessageCompleted { text } => {
Event::AssistantMessage { text: text.clone() }
}
SessionEvent::ToolQueued {
tool_call_id,
tool_name,
summary,
mutability,
input_json,
} => Event::ToolQueued {
tool_call_id: tool_call_id.clone(),
tool_name: tool_name.clone(),
summary: summary.clone(),
mutability: mutability_of(*mutability),
input: json_or_string(input_json),
},
SessionEvent::ToolStarted {
tool_call_id,
tool_name,
} => Event::ToolStarted {
tool_call_id: tool_call_id.clone(),
tool_name: tool_name.clone(),
},
SessionEvent::ToolProgress {
tool_call_id,
tool_name,
progress,
} => Event::ToolProgress {
tool_call_id: tool_call_id.clone(),
tool_name: tool_name.clone(),
progress: progress.clone(),
},
SessionEvent::ToolCompleted {
tool_call_id,
tool_name,
summary,
is_error,
} => Event::ToolCompleted {
tool_call_id: tool_call_id.clone(),
tool_name: tool_name.clone(),
summary: summary.clone(),
is_error: *is_error,
},
SessionEvent::PermissionRequested {
request_id,
tool_call_id,
tool_name,
description,
preview,
classification: _,
} => Event::PermissionRequested {
request_id: request_id.clone(),
tool_call_id: tool_call_id.clone(),
tool_name: tool_name.clone(),
description: description.clone(),
preview: json_or_string(preview),
},
SessionEvent::PermissionResolved {
request_id,
tool_call_id,
tool_name,
outcome,
rule_scope,
} => Event::PermissionResolved {
request_id: request_id.clone(),
tool_call_id: tool_call_id.clone(),
tool_name: tool_name.clone(),
outcome: outcome_of(*outcome),
rule_scope: rule_scope.map(scope_of),
},
SessionEvent::TaskUpdated {
task_id,
kind,
status,
title,
detail,
} => Event::TaskUpdated {
task_id: task_id.clone(),
kind: task_kind_of(*kind),
status: task_status_of(*status),
title: title.clone(),
detail: detail.clone(),
},
SessionEvent::CompactionStarted { agent_id } => Event::CompactionStarted {
agent_id: agent_id.clone(),
},
SessionEvent::CompactionCompleted {
agent_id,
replaced_items,
preserved_items,
resulting_transcript_len,
extracted_facts_count,
summary_preview,
} => Event::CompactionCompleted {
agent_id: agent_id.clone(),
replaced_items: *replaced_items,
preserved_items: *preserved_items,
transcript_len: *resulting_transcript_len,
extracted_facts: *extracted_facts_count,
summary_preview: summary_preview.clone(),
},
SessionEvent::RequestToolResultsElided {
agent_id,
policy,
canonical_tool_result_content_bytes,
projected_tool_result_content_bytes,
results,
} => Event::RequestToolResultsElided {
agent_id: agent_id.clone(),
policy: elision_policy_of(policy),
canonical_tool_result_content_bytes: *canonical_tool_result_content_bytes,
projected_tool_result_content_bytes: *projected_tool_result_content_bytes,
results: results.iter().map(elided_tool_result_of).collect(),
},
SessionEvent::MemoryUpdated {
agent_id,
stored_records,
} => Event::MemoryUpdated {
agent_id: agent_id.clone(),
stored_records: *stored_records,
},
SessionEvent::UsageReport {
agent_id,
input_tokens,
output_tokens,
cache_read_tokens,
cache_creation_tokens,
reasoning_tokens,
thoughts_tokens,
} => Event::Usage {
agent_id: agent_id.clone(),
input_tokens: *input_tokens,
output_tokens: *output_tokens,
cache_read_tokens: *cache_read_tokens,
cache_creation_tokens: *cache_creation_tokens,
reasoning_tokens: *reasoning_tokens,
thoughts_tokens: *thoughts_tokens,
},
SessionEvent::Notice { severity, message }
if is_refused_memory_write(severity, message) =>
{
return None;
}
SessionEvent::Notice { severity, message } => Event::Notice {
severity: severity_of(*severity),
message: message.clone(),
},
SessionEvent::RetryAttempt {
agent_id,
error_message,
attempt,
max_attempts,
next_delay_ms,
} => Event::Retry {
agent_id: agent_id.clone(),
error: error_message.clone(),
attempt: *attempt,
max_attempts: *max_attempts,
next_delay_ms: *next_delay_ms,
},
SessionEvent::Branched {
entry_id,
abandoned_entries,
} => Event::Branched {
entry_id: entry_id.clone(),
abandoned_entries: *abandoned_entries,
},
SessionEvent::Error {
message,
recoverable,
} => Event::Error {
message: message.clone(),
recoverable: *recoverable,
},
};
Some(mapped)
}
fn json_or_string(raw: &str) -> Value {
serde_json::from_str(raw).unwrap_or_else(|_| Value::String(raw.to_string()))
}
fn mutability_of(value: ToolMutability) -> Mutability {
match value {
ToolMutability::ReadOnly => Mutability::ReadOnly,
ToolMutability::Mutating => Mutability::Mutating,
ToolMutability::Unknown => Mutability::Unknown,
}
}
fn outcome_of(value: MentraPermissionOutcome) -> PermissionOutcome {
match value {
MentraPermissionOutcome::Allowed => PermissionOutcome::Allowed,
MentraPermissionOutcome::Denied => PermissionOutcome::Denied,
}
}
fn scope_of(value: PermissionRuleScope) -> RuleScope {
match value {
PermissionRuleScope::Process => RuleScope::Process,
PermissionRuleScope::Session => RuleScope::Session,
PermissionRuleScope::Project => RuleScope::Project,
PermissionRuleScope::Global => RuleScope::Global,
}
}
fn task_kind_of(value: MentraTaskKind) -> TaskKind {
match value {
MentraTaskKind::Subagent => TaskKind::Subagent,
MentraTaskKind::BackgroundTask => TaskKind::BackgroundTask,
MentraTaskKind::Teammate => TaskKind::Teammate,
}
}
fn task_status_of(value: TaskLifecycleStatus) -> TaskStatus {
match value {
TaskLifecycleStatus::Spawned => TaskStatus::Spawned,
TaskLifecycleStatus::Running => TaskStatus::Running,
TaskLifecycleStatus::Finished => TaskStatus::Finished,
TaskLifecycleStatus::Failed => TaskStatus::Failed,
}
}
fn elision_policy_of(
value: &MentraRequestToolResultElisionPolicy,
) -> RequestToolResultElisionPolicy {
match value {
MentraRequestToolResultElisionPolicy::KeepRecent {
configured_keep_recent_tool_results,
} => RequestToolResultElisionPolicy::KeepRecent {
configured_keep_recent_tool_results: *configured_keep_recent_tool_results,
},
MentraRequestToolResultElisionPolicy::ByteBudget {
configured_max_bytes,
configured_prioritize_recent_results,
configured_max_preview_bytes,
} => RequestToolResultElisionPolicy::ByteBudget {
configured_max_bytes: *configured_max_bytes,
configured_prioritize_recent_results: *configured_prioritize_recent_results,
configured_max_preview_bytes: *configured_max_preview_bytes,
},
}
}
fn elided_tool_result_of(value: &MentraElidedToolResult) -> ElidedToolResult {
ElidedToolResult {
tool_call_id: value.tool_call_id.clone(),
tool_name: value.tool_name.clone(),
is_error: value.is_error,
canonical_content_kind: content_kind_of(value.canonical_content_kind),
action: elision_action_of(value.action),
canonical_content_bytes: value.canonical_content_bytes,
projected_content_bytes: value.projected_content_bytes,
}
}
fn content_kind_of(value: MentraToolResultContentKind) -> ToolResultContentKind {
match value {
MentraToolResultContentKind::Text => ToolResultContentKind::Text,
MentraToolResultContentKind::Structured => ToolResultContentKind::Structured,
}
}
fn elision_action_of(value: MentraToolResultElisionAction) -> ToolResultElisionAction {
match value {
MentraToolResultElisionAction::Preview => ToolResultElisionAction::Preview,
MentraToolResultElisionAction::Marker => ToolResultElisionAction::Marker,
MentraToolResultElisionAction::Omitted => ToolResultElisionAction::Omitted,
}
}
fn is_refused_memory_write(severity: &MentraNoticeSeverity, message: &str) -> bool {
const REFUSAL: &str = "runtime store error: FileRuntimeStore does not persist long-term memory";
matches!(severity, MentraNoticeSeverity::Warning) && message.contains(REFUSAL)
}
fn severity_of(value: MentraNoticeSeverity) -> NoticeSeverity {
match value {
MentraNoticeSeverity::Info => NoticeSeverity::Info,
MentraNoticeSeverity::Warning => NoticeSeverity::Warning,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn session_started_is_already_covered_by_the_header() {
let event = SessionEvent::SessionStarted {
session_id: mentra::SessionId::new(),
};
assert_eq!(from_session_event(&event), None);
}
#[test]
fn the_refused_memory_write_is_a_decision_not_a_run_fact() {
let refused = SessionEvent::Notice {
severity: MentraNoticeSeverity::Warning,
message: refused_memory_write(),
};
assert_eq!(from_session_event(&refused), None);
}
fn refused_memory_write() -> String {
"agent 'a-1': runtime store error: FileRuntimeStore does not persist long-term memory; \
enable mentra's `store-sqlite` feature and use SqliteRuntimeStore or HybridRuntimeStore \
for durable memory"
.to_string()
}
#[test]
fn only_a_warning_carrying_that_exact_refusal_is_dropped() {
let elsewhere = SessionEvent::Notice {
severity: MentraNoticeSeverity::Info,
message: refused_memory_write(),
};
assert_eq!(
from_session_event(&elsewhere),
Some(Event::Notice {
severity: NoticeSeverity::Info,
message: refused_memory_write(),
}),
"the same text at a severity upstream never sends it at did not come from there"
);
for message in [
"agent 'a-1': runtime store error: could not save the memory cursor",
"agent 'a-1': long-term memory is unavailable here",
"something else worth hearing",
] {
let ordinary = SessionEvent::Notice {
severity: MentraNoticeSeverity::Warning,
message: message.to_string(),
};
assert_eq!(
from_session_event(&ordinary),
Some(Event::Notice {
severity: NoticeSeverity::Warning,
message: message.to_string(),
}),
"{message}"
);
}
}
#[test]
fn token_deltas_carry_only_the_delta() {
let event = SessionEvent::AssistantTokenDelta {
delta: "lo".to_string(),
full_text: "hello".to_string(),
};
assert_eq!(
from_session_event(&event),
Some(Event::AssistantDelta {
text: "lo".to_string()
})
);
}
#[test]
fn byte_budget_elision_maps_every_public_fact_without_a_body() {
let event = SessionEvent::RequestToolResultsElided {
agent_id: "agent-1".to_string(),
policy: MentraRequestToolResultElisionPolicy::ByteBudget {
configured_max_bytes: 4_096,
configured_prioritize_recent_results: 2,
configured_max_preview_bytes: 512,
},
canonical_tool_result_content_bytes: 9_000,
projected_tool_result_content_bytes: 4_000,
results: vec![MentraElidedToolResult {
tool_call_id: "call-1".to_string(),
tool_name: Some("grep".to_string()),
is_error: false,
canonical_content_kind: MentraToolResultContentKind::Text,
action: MentraToolResultElisionAction::Preview,
canonical_content_bytes: 8_000,
projected_content_bytes: 3_000,
}],
};
assert_eq!(
from_session_event(&event),
Some(Event::RequestToolResultsElided {
agent_id: "agent-1".to_string(),
policy: RequestToolResultElisionPolicy::ByteBudget {
configured_max_bytes: 4_096,
configured_prioritize_recent_results: 2,
configured_max_preview_bytes: 512,
},
canonical_tool_result_content_bytes: 9_000,
projected_tool_result_content_bytes: 4_000,
results: vec![ElidedToolResult {
tool_call_id: "call-1".to_string(),
tool_name: Some("grep".to_string()),
is_error: false,
canonical_content_kind: ToolResultContentKind::Text,
action: ToolResultElisionAction::Preview,
canonical_content_bytes: 8_000,
projected_content_bytes: 3_000,
}],
})
);
}
#[test]
fn recent_count_elision_keeps_omitted_structured_result_metadata() {
let event = SessionEvent::RequestToolResultsElided {
agent_id: "agent-2".to_string(),
policy: MentraRequestToolResultElisionPolicy::KeepRecent {
configured_keep_recent_tool_results: 3,
},
canonical_tool_result_content_bytes: 1_024,
projected_tool_result_content_bytes: 24,
results: vec![MentraElidedToolResult {
tool_call_id: "call-2".to_string(),
tool_name: None,
is_error: true,
canonical_content_kind: MentraToolResultContentKind::Structured,
action: MentraToolResultElisionAction::Omitted,
canonical_content_bytes: 1_024,
projected_content_bytes: 0,
}],
};
let Some(Event::RequestToolResultsElided {
policy, results, ..
}) = from_session_event(&event)
else {
panic!("expected request_tool_results_elided");
};
assert_eq!(
policy,
RequestToolResultElisionPolicy::KeepRecent {
configured_keep_recent_tool_results: 3,
}
);
assert_eq!(
results[0].canonical_content_kind,
ToolResultContentKind::Structured
);
assert_eq!(results[0].action, ToolResultElisionAction::Omitted);
assert!(results[0].is_error);
}
#[test]
fn tool_input_is_parsed_into_real_json() {
let event = SessionEvent::ToolQueued {
tool_call_id: "c1".to_string(),
tool_name: "shell".to_string(),
summary: "run ls".to_string(),
mutability: ToolMutability::ReadOnly,
input_json: r#"{"command":"ls"}"#.to_string(),
};
let Some(Event::ToolQueued { input, .. }) = from_session_event(&event) else {
panic!("expected a tool_queued event");
};
assert_eq!(input["command"], "ls");
}
#[test]
fn unparseable_tool_input_survives_as_a_string() {
let event = SessionEvent::ToolQueued {
tool_call_id: "c1".to_string(),
tool_name: "shell".to_string(),
summary: String::new(),
mutability: ToolMutability::Unknown,
input_json: "not json {".to_string(),
};
let Some(Event::ToolQueued { input, .. }) = from_session_event(&event) else {
panic!("expected a tool_queued event");
};
assert_eq!(input, Value::String("not json {".to_string()));
}
#[test]
fn permission_resolution_keeps_the_remembered_scope() {
let event = SessionEvent::PermissionResolved {
request_id: "r1".to_string(),
tool_call_id: "c1".to_string(),
tool_name: "shell".to_string(),
outcome: MentraPermissionOutcome::Allowed,
rule_scope: Some(PermissionRuleScope::Project),
};
let Some(Event::PermissionResolved {
outcome,
rule_scope,
..
}) = from_session_event(&event)
else {
panic!("expected a permission_resolved event");
};
assert_eq!(outcome, PermissionOutcome::Allowed);
assert_eq!(rule_scope, Some(RuleScope::Project));
}
#[test]
fn permission_resolution_maps_the_process_scope_mentra_027_added() {
let event = SessionEvent::PermissionResolved {
request_id: "r1".to_string(),
tool_call_id: "c1".to_string(),
tool_name: "shell".to_string(),
outcome: MentraPermissionOutcome::Denied,
rule_scope: Some(PermissionRuleScope::Process),
};
let Some(Event::PermissionResolved {
outcome,
rule_scope,
..
}) = from_session_event(&event)
else {
panic!("expected a permission_resolved event");
};
assert_eq!(outcome, PermissionOutcome::Denied);
assert_eq!(rule_scope, Some(RuleScope::Process));
}
#[test]
fn errors_keep_their_recoverability() {
let event = SessionEvent::Error {
message: "rate limited".to_string(),
recoverable: true,
};
assert_eq!(
from_session_event(&event),
Some(Event::Error {
message: "rate limited".to_string(),
recoverable: true,
})
);
}
}