use serde::{Deserialize, Serialize};
use rho_sdk::model::{ContextUsage, ModelUsage};
use crate::{run_artifacts::AttachmentEvent, subagent::RunState};
pub(crate) const MAX_TEXT_DELTA_CHARS: usize = 32 * 1024;
pub(crate) const MAX_RESULT_CHARS: usize = 64 * 1024;
pub(crate) const MAX_TOOL_PAYLOAD_CHARS: usize = 16 * 1024;
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum StreamEffect {
Attachment(AttachmentEvent),
Status(StatusPatch),
RateLimit(RateLimitInfo),
Terminal(TerminalResult),
}
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct StatusPatch {
pub(crate) state: Option<RunState>,
pub(crate) turns: Option<u64>,
pub(crate) input_tokens: Option<u64>,
pub(crate) output_tokens: Option<u64>,
pub(crate) last_activity: Option<String>,
pub(crate) append_text: Option<String>,
pub(crate) result: Option<String>,
pub(crate) error: Option<String>,
pub(crate) claude_session_id: Option<String>,
pub(crate) claude_model: Option<String>,
pub(crate) total_cost_usd: Option<f64>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum TerminalClassification {
Success {
subtype: String,
},
Failure {
subtype: String,
is_error: bool,
},
Invalid {
reason: String,
},
}
#[cfg(test)]
impl TerminalClassification {
pub(crate) fn is_success(&self) -> bool {
matches!(self, Self::Success { .. })
}
pub(crate) fn is_failure(&self) -> bool {
matches!(self, Self::Failure { .. })
}
pub(crate) fn is_invalid(&self) -> bool {
matches!(self, Self::Invalid { .. })
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct TerminalResult {
pub(crate) classification: TerminalClassification,
pub(crate) result_text: Option<String>,
pub(crate) error: Option<String>,
pub(crate) session_id: Option<String>,
pub(crate) num_turns: Option<u64>,
pub(crate) usage: Option<ModelUsage>,
pub(crate) context: Option<ContextUsage>,
pub(crate) total_cost_usd: Option<f64>,
pub(crate) permission_denials: Vec<String>,
pub(crate) stop_reason: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct RateLimitInfo {
#[serde(default)]
pub(crate) status: Option<String>,
#[serde(default)]
pub(crate) rate_limit_type: Option<String>,
#[serde(default)]
pub(crate) resets_at: Option<i64>,
#[serde(default)]
pub(crate) utilization: Option<f64>,
#[serde(default)]
pub(crate) overage_status: Option<String>,
#[serde(default)]
pub(crate) overage_resets_at: Option<i64>,
#[serde(default)]
pub(crate) is_using_overage: Option<bool>,
}
impl RateLimitInfo {
pub(crate) fn window_key(&self) -> &str {
self.rate_limit_type
.as_deref()
.filter(|value| !value.is_empty())
.unwrap_or("usage_window")
}
pub(crate) fn remaining_percent(&self) -> Option<f64> {
let used = self.utilization?;
if !used.is_finite() {
return None;
}
Some(((1.0 - used) * 100.0).clamp(0.0, 100.0))
}
}
pub(crate) fn classify_terminal_result(
subtype: Option<&str>,
is_error: Option<bool>,
) -> TerminalClassification {
let Some(subtype) = subtype.filter(|value| !value.is_empty()) else {
return TerminalClassification::Invalid {
reason: match is_error {
Some(true) => "result missing subtype (is_error=true)".into(),
Some(false) => "result missing subtype (is_error=false)".into(),
None => "result missing subtype and is_error".into(),
},
};
};
let Some(is_error) = is_error else {
return TerminalClassification::Invalid {
reason: format!("result subtype `{subtype}` missing is_error"),
};
};
if subtype == "success" && !is_error {
TerminalClassification::Success {
subtype: subtype.to_string(),
}
} else {
TerminalClassification::Failure {
subtype: subtype.to_string(),
is_error,
}
}
}