use crate::{cancellation::AgentCancellation, providers::ToolCall, tools::ToolResult};
use std::{path::PathBuf, sync::Arc};
use super::redact_sensitive_text;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ContextUsageSource {
FallbackEstimate,
TokenizerEstimate,
FallbackProjection,
TokenizerProjection,
LastProviderUsage,
ProviderExact,
ProviderPartial,
}
#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(default)]
pub(crate) struct NormalizedUsageSnapshot {
pub(crate) effective_input: u64,
pub(crate) output: u64,
pub(crate) cache_read: u64,
pub(crate) cache_known: bool,
}
#[derive(Debug, Clone, Copy, Default, Serialize, PartialEq, Eq)]
pub(crate) struct NormalizedUsageAggregate {
pub(crate) whole_run: NormalizedUsageSnapshot,
pub(crate) latest: Option<NormalizedUsageSnapshot>,
#[serde(default)]
pub(crate) latest_request_sequence: Option<u64>,
#[serde(default)]
pub(crate) latest_final: bool,
}
impl<'de> Deserialize<'de> for NormalizedUsageAggregate {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
if value.get("whole_run").is_some()
|| value.get("latest").is_some()
|| value.get("latest_request_sequence").is_some()
|| value.get("latest_final").is_some()
{
#[derive(Default, Deserialize)]
#[serde(default)]
struct AggregateFields {
whole_run: NormalizedUsageSnapshot,
latest: Option<NormalizedUsageSnapshot>,
latest_request_sequence: Option<u64>,
latest_final: bool,
}
let fields = serde_json::from_value::<AggregateFields>(value)
.map_err(serde::de::Error::custom)?;
Ok(Self {
whole_run: fields.whole_run,
latest: fields.latest,
latest_request_sequence: fields.latest_request_sequence,
latest_final: fields.latest_final,
})
} else {
let snapshot = serde_json::from_value::<NormalizedUsageSnapshot>(value)
.map_err(serde::de::Error::custom)?;
Ok(Self {
whole_run: snapshot,
latest: Some(snapshot),
latest_request_sequence: None,
latest_final: false,
})
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ProviderContextInjectionDisplay {
pub(crate) phase: crate::hooks::HookPhase,
pub(crate) label: String,
pub(crate) target: String,
pub(crate) status: String,
pub(crate) item_count: usize,
pub(crate) byte_count: usize,
pub(crate) content: Option<String>,
}
impl ProviderContextInjectionDisplay {
pub(crate) fn new(
phase: crate::hooks::HookPhase,
label: impl Into<String>,
target: impl Into<String>,
status: impl Into<String>,
item_count: usize,
byte_count: usize,
content: Option<String>,
) -> Self {
Self {
phase,
label: redact_sensitive_text(&label.into()),
target: redact_sensitive_text(&target.into()),
status: redact_sensitive_text(&status.into()),
item_count,
byte_count,
content: content.map(|text| redact_sensitive_text(&text)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum UserPromptOrigin {
User,
Steering,
AutomaticCompaction,
}
impl UserPromptOrigin {
pub(crate) const fn label(self) -> &'static str {
match self {
Self::User => "user",
Self::Steering => "steering",
Self::AutomaticCompaction => "automatic_compaction",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum OutputEvent {
FastObservation {
provider_id: String,
model: String,
requested_service_tier: String,
outcome: crate::fast::FastOutcome,
request_sequence: u64,
run_order: Option<u64>,
},
SessionHeader {
session_id: Option<String>,
model: String,
cwd: PathBuf,
},
UserPrompt {
text: String,
},
AutomaticUserPrompt {
text: String,
},
CompactionTriggered {
current_tokens: usize,
max_tokens: usize,
threshold: String,
},
CompactionStarted,
CompactionCompleted {
current_tokens: usize,
max_tokens: usize,
summary: String,
},
CompactionFailed {
message: String,
canceled: bool,
},
BashCommand {
command: String,
},
ContextUsage {
current_tokens: usize,
max_tokens: usize,
reasoning_tokens: Option<usize>,
source: ContextUsageSource,
request_sequence: u64,
},
UsageSnapshot {
usage: NormalizedUsageSnapshot,
request_sequence: u64,
final_usage: bool,
},
ThinkingSummaryDelta {
text: String,
},
ThinkingSummaryComplete {
text: String,
},
ThinkingSummaryCompleteIdentified {
text: String,
item_id: Option<String>,
turn_id: Option<String>,
},
AssistantDelta {
text: String,
},
AssistantComplete {
text: String,
},
ToolStarted {
call: Box<ToolCall>,
label: String,
},
Diagnostic {
level: String,
message: String,
},
HookDiagnostic {
diagnostic: Box<crate::hooks::HookDiagnostic>,
},
ProviderContextInjection {
metadata: ProviderContextInjectionDisplay,
},
SubdirInstructionInjection {
path: PathBuf,
bytes: usize,
parent_activity_id: Option<String>,
},
ToolResult {
call: Box<ToolCall>,
result: Box<ToolResult>,
summary: Box<ToolDisplaySummary>,
changed_paths: Vec<PathBuf>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ToolActivityDetail {
pub(crate) tool_name: Arc<str>,
pub(crate) label: Arc<str>,
pub(crate) params: serde_json::Value,
pub(crate) metadata: serde_json::Value,
pub(crate) status: ActivityStatus,
pub(crate) output: Arc<str>,
pub(crate) applied_diff: Option<Arc<str>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ToolDisplaySummary {
pub tool_name: String,
pub status: ToolStatus,
pub unicode_mark: &'static str,
pub ascii_mark: &'static str,
pub label: String,
pub metadata: Vec<(String, String)>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ToolStatus {
Running,
Writing,
Success,
Failure,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub(crate) struct ActivityId(pub(crate) String);
impl ActivityId {
pub(crate) fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub(crate) fn child(&self, segment: impl AsRef<str>) -> Self {
Self(format!("{}/{}", self.0, segment.as_ref()))
}
pub(crate) fn as_str(&self) -> &str {
&self.0
}
pub(crate) fn path_suffix_after(&self, ancestor: &Self) -> Option<&str> {
self.0
.strip_prefix(&ancestor.0)
.and_then(|rest| rest.strip_prefix('/'))
}
pub(crate) fn is_path_descendant_of(&self, ancestor: &Self) -> bool {
self.path_suffix_after(ancestor).is_some()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ActivityKind {
Tool,
Hook,
ProviderContextInjection,
SubagentBatch,
SubagentTask,
Assistant,
Diagnostic,
Compaction,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ActivityStatus {
Queued,
Running,
Writing,
Success,
Failed,
Canceled,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ActivityMetadata {
pub(crate) label: String,
pub(crate) detail: Option<String>,
pub(crate) fields: Vec<(String, String)>,
}
impl ActivityMetadata {
pub(crate) fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
detail: None,
fields: Vec::new(),
}
}
pub(crate) fn reasoning(lines: &[String]) -> Self {
match lines {
[] => Self::new("reasoning"),
[line] => {
let mut metadata = Self::new(format!("reasoning • {line}"));
metadata.detail = Some(line.clone());
metadata
}
_ => {
let mut metadata = Self::new(format!("reasoning summaries ×{}", lines.len()));
metadata.detail = Some(
lines
.iter()
.enumerate()
.map(|(index, line)| format!("{}. {line}", index + 1))
.collect::<Vec<_>>()
.join("\n"),
);
metadata
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ActivityEvent {
Started {
id: ActivityId,
parent_id: Option<ActivityId>,
kind: ActivityKind,
status: ActivityStatus,
metadata: ActivityMetadata,
},
Delta {
id: ActivityId,
preview: String,
},
UsageUpdate {
id: ActivityId,
current_tokens: usize,
max_tokens: usize,
reasoning_tokens: Option<usize>,
source: ContextUsageSource,
request_sequence: u64,
},
UsageSnapshot {
id: ActivityId,
usage: NormalizedUsageAggregate,
request_sequence: u64,
final_usage: bool,
},
ToolStartedDetail {
id: ActivityId,
detail: ToolActivityDetail,
},
ToolResultDetail {
id: ActivityId,
detail: ToolActivityDetail,
},
FinalPreview {
id: ActivityId,
preview: String,
metadata: Option<ActivityMetadata>,
status: Option<ActivityStatus>,
},
Finished {
id: ActivityId,
status: ActivityStatus,
metadata: Option<ActivityMetadata>,
},
FastObservation {
provider_id: String,
model: String,
requested_service_tier: String,
outcome: crate::fast::FastOutcome,
request_sequence: u64,
run_order: Option<u64>,
},
}
pub(crate) type ActivitySender = std::sync::Arc<dyn Fn(ActivityEvent) + Send + Sync>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum InvocationMode {
Print,
MissionControl,
Subagent,
}
impl InvocationMode {
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Print => "print",
Self::MissionControl => "mission_control",
Self::Subagent => "subagent",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct HookContextMetadata {
pub(crate) session_id: Option<String>,
pub(crate) session_path: Option<PathBuf>,
pub(crate) provider_id: Option<String>,
pub(crate) model_id: Option<String>,
pub(crate) agent_id: Option<String>,
pub(crate) invocation_mode: InvocationMode,
pub(crate) turn_id: Option<String>,
pub(crate) message_id: Option<String>,
pub(crate) subagent: bool,
}
impl HookContextMetadata {
pub(crate) fn new(invocation_mode: InvocationMode) -> Self {
Self {
session_id: None,
session_path: None,
provider_id: None,
model_id: None,
agent_id: None,
invocation_mode,
turn_id: None,
message_id: None,
subagent: matches!(invocation_mode, InvocationMode::Subagent),
}
}
pub(crate) fn for_tool_call(
&self,
turn_id: impl Into<String>,
message_id: impl Into<String>,
) -> Self {
let mut cloned = self.clone();
cloned.turn_id = Some(turn_id.into());
cloned.message_id = Some(message_id.into());
cloned
}
}
#[derive(Clone)]
pub(crate) struct ToolDispatchContext {
pub(crate) parent_activity_id: Option<ActivityId>,
pub(crate) activity_sender: Option<ActivitySender>,
pub(crate) hook_context: HookContextMetadata,
pub(crate) cancellation: AgentCancellation,
}
impl ToolDispatchContext {
pub(crate) fn new(
parent_activity_id: Option<ActivityId>,
activity_sender: Option<ActivitySender>,
) -> Self {
Self::new_with_hook_context(
parent_activity_id,
activity_sender,
HookContextMetadata::new(InvocationMode::Print),
)
}
pub(crate) fn new_with_hook_context(
parent_activity_id: Option<ActivityId>,
activity_sender: Option<ActivitySender>,
hook_context: HookContextMetadata,
) -> Self {
Self::new_with_hook_context_and_cancellation(
parent_activity_id,
activity_sender,
hook_context,
AgentCancellation::default(),
)
}
pub(crate) fn new_with_hook_context_and_cancellation(
parent_activity_id: Option<ActivityId>,
activity_sender: Option<ActivitySender>,
hook_context: HookContextMetadata,
cancellation: AgentCancellation,
) -> Self {
Self {
parent_activity_id,
activity_sender,
hook_context,
cancellation,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn activity_id_child_and_path_ancestry_preserve_existing_strings() {
let parent = ActivityId::new("a");
assert_eq!(parent.child("g1").as_str(), "a/g1");
assert_eq!(parent.child("g1/reasoning").as_str(), "a/g1/reasoning");
assert!(ActivityId::new("a/b").is_path_descendant_of(&parent));
assert!(!ActivityId::new("ab").is_path_descendant_of(&parent));
assert!(!parent.is_path_descendant_of(&parent));
}
}