use std::any::Any;
use std::collections::BTreeMap;
use chrono::{SecondsFormat, Utc};
use serde_json::Value;
use crate::types::{AgentStatus, SubTaskOutcome};
pub(super) fn status_label(status: AgentStatus) -> &'static str {
match status {
AgentStatus::Pending => "pending",
AgentStatus::Running => "running",
AgentStatus::WaitUser => "wait_user",
AgentStatus::Completed => "completed",
AgentStatus::Failed => "failed",
AgentStatus::MaxCycles => "max_cycles",
AgentStatus::ReconciliationRequired => "reconciliation_required",
}
}
pub(super) fn normalize_failed_outcome(mut outcome: SubTaskOutcome) -> SubTaskOutcome {
if outcome.status == AgentStatus::Failed
&& outcome
.error_code
.as_deref()
.is_none_or(|error_code| error_code.trim().is_empty())
{
outcome.error_code = Some("sub_task_failed".to_string());
}
outcome
}
pub(super) fn now_iso() -> String {
Utc::now().to_rfc3339_opts(SecondsFormat::Micros, false)
}
pub(super) fn panic_payload_to_string(payload: &(dyn Any + Send)) -> String {
if let Some(message) = payload.downcast_ref::<&str>() {
return (*message).to_string();
}
if let Some(message) = payload.downcast_ref::<String>() {
return message.clone();
}
"sub-task runner panicked".to_string()
}
pub(super) fn payload_u32(payload: &BTreeMap<String, Value>, key: &str) -> Option<u32> {
payload
.get(key)
.and_then(Value::as_u64)
.and_then(|value| u32::try_from(value).ok())
}
pub(super) fn preview_text(value: Option<&Value>) -> Option<String> {
const PREVIEW_LIMIT: usize = 240;
let text = match value? {
Value::Null => return None,
Value::String(value) => value.clone(),
other => other.to_string(),
};
let text = text.trim();
if text.is_empty() {
return None;
}
if text.chars().count() <= PREVIEW_LIMIT {
return Some(text.to_string());
}
let mut truncated = text
.chars()
.take(PREVIEW_LIMIT.saturating_sub(3))
.collect::<String>();
truncated = truncated.trim_end().to_string();
truncated.push_str("...");
Some(truncated)
}
pub(super) fn is_internal_workspace_file(path: &str) -> bool {
let normalized = path.trim().trim_matches('/');
normalized.is_empty()
|| normalized
.split('/')
.any(|part| part.is_empty() || part.starts_with('.'))
}