use std::{path::PathBuf, sync::Arc};
use chrono::{DateTime, Utc};
use serde_json::Value;
use crate::config::ApiProvider;
use crate::error_taxonomy::ErrorEnvelope;
use crate::models::{Message, SystemPrompt, Tool, Usage};
use crate::tools::goal::GoalSnapshot;
use crate::tools::spec::{ToolError, ToolResult};
use crate::tools::subagent::{AgentWorkerStatus, CoordinationDetailProjection, SubAgentResult};
use crate::tools::user_input::UserInputRequest;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TurnOutcomeStatus {
Completed,
Interrupted,
Failed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TurnRoute {
pub provider: ApiProvider,
pub provider_identity: String,
pub model: String,
pub auto_model: bool,
pub receipt: Option<crate::route_receipt::TurnRouteReceipt>,
pub billing: Option<RouteBillingEnvelope>,
pub base_url: String,
pub billing_product: crate::route_billing::RouteProduct,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RouteBillingEnvelope {
pub billing_surface: Option<String>,
pub endpoint_fingerprint: Option<String>,
pub billing_mode: crate::cost_status::RouteBillingMode,
pub dispatched_at: DateTime<Utc>,
}
impl TurnRoute {
#[must_use]
pub fn cost_envelope(&self) -> Option<crate::cost_status::EffectiveRouteEnvelope> {
let billing = self.billing.as_ref()?;
Some(crate::cost_status::EffectiveRouteEnvelope {
provider: self.provider,
provider_identity: self.provider_identity.clone(),
model: self.model.clone(),
billing_surface: billing.billing_surface.clone(),
endpoint_fingerprint: billing.endpoint_fingerprint.clone(),
billing_mode: billing.billing_mode,
dispatched_at: billing.dispatched_at,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AgentProgressEventMeta {
pub worker_status: AgentWorkerStatus,
pub step: Option<u32>,
pub tool_name: Option<String>,
}
impl AgentProgressEventMeta {
#[must_use]
pub const fn new(worker_status: AgentWorkerStatus) -> Self {
Self {
worker_status,
step: None,
tool_name: None,
}
}
#[must_use]
pub const fn with_step(mut self, step: u32) -> Self {
self.step = Some(step);
self
}
#[must_use]
pub fn with_tool(mut self, tool_name: impl Into<String>) -> Self {
self.tool_name = Some(tool_name.into());
self
}
}
#[derive(Debug, Clone)]
pub enum Event {
MessageStarted {
#[allow(dead_code)]
index: usize,
},
MessageDelta {
#[allow(dead_code)]
index: usize,
content: String,
},
MessageComplete {
#[allow(dead_code)]
index: usize,
},
ThinkingStarted {
#[allow(dead_code)]
index: usize,
},
ThinkingDelta {
#[allow(dead_code)]
index: usize,
content: String,
},
ThinkingComplete {
#[allow(dead_code)]
index: usize,
},
ToolCallStarted {
id: String,
name: String,
input: Value,
},
ToolCallHeartbeat,
ToolCallComplete {
id: String,
name: String,
result: Result<ToolResult, ToolError>,
},
TurnStarted {
turn_id: String,
created_at: DateTime<Utc>,
route: Option<TurnRoute>,
},
ToolRequestSnapshot {
snapshot: crate::tool_inspection::ToolInspectionSnapshot,
},
RouteDispatched { turn_id: String, route: TurnRoute },
TurnComplete {
usage: Usage,
status: TurnOutcomeStatus,
error: Option<String>,
tool_catalog: Option<Vec<Tool>>,
base_url: Option<String>,
},
TurnUsage {
usage: Usage,
duration_ms: u64,
first_token_ms: Option<u64>,
request_ms: Option<u64>,
},
GoalUpdated { snapshot: GoalSnapshot },
CompactionStarted {
id: String,
auto: bool,
message: String,
},
CompactionCompleted {
id: String,
auto: bool,
message: String,
#[allow(dead_code)]
messages_before: Option<usize>,
#[allow(dead_code)]
messages_after: Option<usize>,
summary_prompt: Option<String>,
},
PurgeStarted {
message: String,
},
PurgeCompleted {
messages_before: usize,
messages_after: usize,
removed_count: usize,
replaced_count: usize,
message: String,
},
PurgeFailed { message: String },
CompactionFailed {
id: String,
auto: bool,
message: String,
},
AgentSpawned {
id: String,
prompt: String,
parent_run_id: Option<String>,
spawn_depth: u32,
model: String,
route_source: Option<String>,
},
AgentProgress {
id: String,
status: String,
activity: AgentProgressEventMeta,
parent_run_id: Option<String>,
spawn_depth: u32,
},
AgentComplete { id: String, result: String },
SubAgentFollowUp {
agent_id: String,
outcome: Result<crate::tools::subagent::UserFollowUpOutcome, String>,
},
AgentList {
agents: Vec<SubAgentResult>,
coordination: CoordinationDetailProjection,
queued_follow_ups: std::collections::HashMap<String, usize>,
},
SubAgentMailbox {
turn_id: String,
seq: u64,
message: crate::tools::subagent::MailboxMessage,
},
WorkflowUi {
run_id: String,
event: Value,
},
Error {
envelope: ErrorEnvelope,
#[allow(dead_code)]
recoverable: bool,
},
Status { message: String },
RequestManifestReady { rendered: String },
PauseEvents {
ack: Option<Arc<tokio::sync::Notify>>,
},
ResumeEvents,
ApprovalRequired {
id: String,
tool_name: String,
description: String,
input: Value,
approval_key: String,
approval_grouping_key: String,
intent_summary: Option<String>,
approval_force_prompt: bool,
},
UserInputRequired {
id: String,
request: UserInputRequest,
},
SessionUpdated {
session_id: String,
messages: Vec<Message>,
system_prompt: Option<SystemPrompt>,
model: String,
workspace: PathBuf,
},
#[allow(dead_code)]
ElevationRequired {
tool_id: String,
tool_name: String,
command: Option<String>,
denial_reason: String,
blocked_network: bool,
blocked_write: bool,
},
LspRepairUpdate {
diagnostics_found: usize,
files: usize,
injected: bool,
},
ToolGateDecision {
agent_id: Option<String>,
tool_id: String,
tool_name: String,
gate: ToolGate,
decision: ToolGateVerdict,
risk: Option<String>,
reason: String,
},
AdvisoryNote {
turn_id: String,
note: String,
tool_call_count: u32,
},
PrefixCacheChange {
description: String,
system_prompt_changed: bool,
tools_changed: bool,
stability_pct: u32,
changed: bool,
pinned_combined_hash: String,
pin_reason: String,
last_miss_reason: String,
context_updates: u64,
},
}
impl Event {
pub fn error(envelope: ErrorEnvelope) -> Self {
let recoverable = envelope.recoverable;
Event::Error {
envelope,
recoverable,
}
}
pub fn status(message: impl Into<String>) -> Self {
Event::Status {
message: message.into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolGate {
AutoReviewDeterministic,
AutoReviewGuardian,
}
impl ToolGate {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::AutoReviewDeterministic => "auto_review_deterministic",
Self::AutoReviewGuardian => "auto_review_guardian",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolGateVerdict {
Allowed,
Denied,
Unavailable,
}
impl ToolGateVerdict {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Allowed => "allowed",
Self::Denied => "denied",
Self::Unavailable => "unavailable",
}
}
}
#[must_use]
pub fn bounded_gate_reason(reason: &str) -> String {
const MAX_CHARS: usize = 220;
let cleaned: String = reason
.chars()
.filter(|c| !c.is_control() && !is_bidi_format_control(*c))
.collect();
let collapsed = cleaned.split_whitespace().collect::<Vec<_>>().join(" ");
if collapsed.chars().count() <= MAX_CHARS {
return collapsed;
}
let mut out: String = collapsed.chars().take(MAX_CHARS - 1).collect();
out.push('…');
out
}
fn is_bidi_format_control(c: char) -> bool {
matches!(
c,
'\u{200E}' | '\u{200F}' | '\u{202A}'..='\u{202E}' | '\u{2066}'..='\u{2069}'
)
}