use crate::{ActorId, RunId};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
pub const MAX_ACTIVITY_TEXT_BYTES: usize = 4096;
pub const MAX_ACTIVITY_BATCH: usize = 100;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ActivityKind {
ToolStarted,
ToolResult,
Progress,
InputRequested,
Heartbeat,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error(
"unknown activity kind {0:?}; expected tool_started, tool_result, progress, input_requested or heartbeat"
)]
pub struct ActivityKindParseError(String);
impl ActivityKind {
#[must_use]
pub fn word(self) -> &'static str {
match self {
Self::ToolStarted => "tool_started",
Self::ToolResult => "tool_result",
Self::Progress => "progress",
Self::InputRequested => "input_requested",
Self::Heartbeat => "heartbeat",
}
}
}
impl std::str::FromStr for ActivityKind {
type Err = ActivityKindParseError;
fn from_str(word: &str) -> Result<Self, Self::Err> {
[
Self::ToolStarted,
Self::ToolResult,
Self::Progress,
Self::InputRequested,
Self::Heartbeat,
]
.into_iter()
.find(|kind| kind.word() == word)
.ok_or_else(|| ActivityKindParseError(word.into()))
}
}
fn sha256_hex(text: &str) -> String {
Sha256::digest(text.as_bytes())
.iter()
.map(|byte| format!("{byte:02x}"))
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ActivityInput {
pub run: RunId,
pub source_sequence: u64,
pub kind: ActivityKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub observed_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NormalizedActivity {
pub text: Option<String>,
pub truncated: bool,
pub text_digest: Option<String>,
}
impl ActivityInput {
#[must_use]
pub fn normalized(&self) -> NormalizedActivity {
let Some(text) = &self.text else {
return NormalizedActivity {
text: None,
truncated: false,
text_digest: None,
};
};
let text_digest = Some(sha256_hex(text));
if text.len() <= MAX_ACTIVITY_TEXT_BYTES {
return NormalizedActivity {
text: Some(text.clone()),
truncated: false,
text_digest,
};
}
let mut kept = String::new();
for character in text.chars() {
if kept.len().saturating_add(character.len_utf8()) > MAX_ACTIVITY_TEXT_BYTES {
break;
}
kept.push(character);
}
NormalizedActivity {
text: Some(kept),
truncated: true,
text_digest,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ActivityRecord {
pub run: RunId,
pub source_sequence: u64,
pub kind: ActivityKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub truncated: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text_digest: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub observed_at: Option<DateTime<Utc>>,
pub recorded_by: ActorId,
pub recorded_at: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ActivityEntry {
pub sequence: u64,
#[serde(flatten)]
pub record: ActivityRecord,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ActivityGap {
pub requested_after: u64,
pub resumes_at: u64,
pub lost: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ActivityPage {
pub entries: Vec<ActivityEntry>,
pub next_after_sequence: u64,
pub head_sequence: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gap: Option<ActivityGap>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ActivityTally {
pub recorded: u64,
pub retained: u64,
pub source_high_water: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub latest: Option<LatestActivity>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LatestActivity {
pub kind: ActivityKind,
pub source_sequence: u64,
pub recorded_at: DateTime<Utc>,
}