use std::path::{Path, PathBuf};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
pub const ENV_ENABLED: &str = "TRUSTY_MEMORY_PROMPT_LOG";
pub const ENV_DIR: &str = "TRUSTY_MEMORY_PROMPT_LOG_DIR";
pub const ENV_MAX_BYTES: &str = "TRUSTY_MEMORY_PROMPT_LOG_MAX_BYTES";
pub const ENV_RETENTION_DAYS: &str = "TRUSTY_MEMORY_PROMPT_LOG_RETENTION_DAYS";
pub const ENV_HASH_PROMPTS: &str = "TRUSTY_MEMORY_PROMPT_LOG_HASH_PROMPTS";
pub const DEFAULT_MAX_BYTES: u64 = 50 * 1024 * 1024;
pub const DEFAULT_RETENTION_DAYS: u32 = 30;
#[derive(Clone, Debug)]
pub struct PromptLogConfig {
pub enabled: bool,
pub dir: PathBuf,
pub max_bytes: u64,
pub retention_days: u32,
pub hash_prompts: bool,
}
impl PromptLogConfig {
pub fn from_env_with_root(data_root: &Path) -> Self {
let enabled = match std::env::var(ENV_ENABLED) {
Ok(v) => !is_off(&v),
Err(_) => true,
};
let dir = match std::env::var(ENV_DIR) {
Ok(d) if !d.trim().is_empty() => PathBuf::from(d),
_ => data_root.join("logs"),
};
let max_bytes = std::env::var(ENV_MAX_BYTES)
.ok()
.and_then(|s| s.trim().parse::<u64>().ok())
.filter(|n| *n > 0)
.unwrap_or(DEFAULT_MAX_BYTES);
let retention_days = std::env::var(ENV_RETENTION_DAYS)
.ok()
.and_then(|s| s.trim().parse::<u32>().ok())
.filter(|n| *n > 0)
.unwrap_or(DEFAULT_RETENTION_DAYS);
let hash_prompts = std::env::var(ENV_HASH_PROMPTS)
.map(|v| is_on(&v))
.unwrap_or(false);
Self {
enabled,
dir,
max_bytes,
retention_days,
hash_prompts,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PromptLogEntry {
pub timestamp: DateTime<Utc>,
pub hook_type: String,
pub injection_kind: String,
pub palace: String,
pub trigger_prompt: String,
pub injection: String,
pub injection_length: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub palace_facts_count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unread_messages_count: Option<usize>,
pub duration_ms: u64,
}
impl PromptLogEntry {
pub fn new(
hook_type: impl Into<String>,
injection_kind: impl Into<String>,
palace: impl Into<String>,
trigger_prompt: impl Into<String>,
injection: impl Into<String>,
) -> Self {
let injection = injection.into();
let injection_length = injection.len();
Self {
timestamp: Utc::now(),
hook_type: hook_type.into(),
injection_kind: injection_kind.into(),
palace: palace.into(),
trigger_prompt: trigger_prompt.into(),
injection,
injection_length,
palace_facts_count: None,
unread_messages_count: None,
duration_ms: 0,
}
}
#[must_use]
pub fn with_duration_ms(mut self, ms: u64) -> Self {
self.duration_ms = ms;
self
}
#[must_use]
pub fn with_palace_facts_count(mut self, n: usize) -> Self {
self.palace_facts_count = Some(n);
self
}
#[must_use]
pub fn with_unread_messages_count(mut self, n: usize) -> Self {
self.unread_messages_count = Some(n);
self
}
}
pub(super) fn is_off(v: &str) -> bool {
matches!(
v.trim().to_ascii_lowercase().as_str(),
"0" | "off" | "false" | "no" | "disabled"
)
}
pub(super) fn is_on(v: &str) -> bool {
matches!(
v.trim().to_ascii_lowercase().as_str(),
"1" | "on" | "true" | "yes" | "enabled"
)
}