use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use serde_json::{Map, Value};
use crate::constants::DEFAULT_CONTEXT_PARAMETER_DESCRIPTION;
use crate::event::Event;
use crate::hooks::ToolCall;
pub(crate) mod env;
#[cfg(test)]
mod tests;
pub type HookError = Box<dyn std::error::Error + Send + Sync + 'static>;
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct UserIdentity {
pub user_id: String,
pub user_name: Option<String>,
pub user_data: Option<Map<String, Value>>,
}
impl UserIdentity {
pub fn new(user_id: impl Into<String>) -> Self {
Self {
user_id: user_id.into(),
user_name: None,
user_data: None,
}
}
#[must_use]
pub fn user_name(mut self, name: impl Into<String>) -> Self {
self.user_name = Some(name.into());
self
}
#[must_use]
pub fn user_data(mut self, data: Map<String, Value>) -> Self {
self.user_data = Some(data);
self
}
}
pub(crate) type HookFuture<T> = Pin<Box<dyn Future<Output = Result<Option<T>, HookError>> + Send>>;
pub(crate) struct Hook<T>(Arc<dyn Fn(ToolCall) -> HookFuture<T> + Send + Sync>);
impl<T: 'static> Hook<T> {
fn new<F, Fut>(hook: F) -> Self
where
F: Fn(ToolCall) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Option<T>, HookError>> + Send + 'static,
{
Self(Arc::new(move |call| Box::pin(hook(call))))
}
pub(crate) fn call(&self, call: ToolCall) -> HookFuture<T> {
let hook = Arc::clone(&self.0);
Box::pin(async move { hook(call).await })
}
}
impl<T> Clone for Hook<T> {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}
impl<T> fmt::Debug for Hook<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt("<hook>", f)
}
}
pub(crate) type RedactFuture<T> = Pin<Box<dyn Future<Output = Result<T, HookError>> + Send>>;
pub(crate) struct Redactor<A, T>(Arc<dyn Fn(A) -> RedactFuture<T> + Send + Sync>);
pub(crate) type StringRedactor = Redactor<String, String>;
pub(crate) type EventRedactor = Redactor<Event, Option<Event>>;
impl<A, T> Redactor<A, T> {
fn new<F, Fut>(hook: F) -> Self
where
F: Fn(A) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<T, HookError>> + Send + 'static,
{
Self(Arc::new(move |input| Box::pin(hook(input))))
}
pub(crate) fn call(&self, input: A) -> RedactFuture<T>
where
A: Send + 'static,
T: 'static,
{
let hook = Arc::clone(&self.0);
Box::pin(async move { hook(input).await })
}
}
impl<A, T> Clone for Redactor<A, T> {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}
impl<A, T> fmt::Debug for Redactor<A, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt("<hook>", f)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum LogDestination {
#[default]
File,
Tracing,
}
#[derive(Clone, Debug)]
#[expect(
clippy::struct_excessive_bools,
reason = "the four boolean switches of the options"
)]
pub struct Options {
report_missing: bool,
tracing: bool,
tool_call_context: bool,
agent_tracking: bool,
custom_context_description: String,
resolve_session_id: Option<Hook<String>>,
identify: Option<Hook<UserIdentity>>,
event_tags: Option<Hook<Map<String, Value>>>,
event_properties: Option<Hook<Map<String, Value>>>,
api_base_url: String,
string_redactor: Option<StringRedactor>,
event_redactor: Option<EventRedactor>,
log_destination: LogDestination,
}
impl Default for Options {
fn default() -> Self {
Self {
report_missing: true,
tracing: true,
tool_call_context: true,
agent_tracking: false,
custom_context_description: String::new(),
resolve_session_id: None,
identify: None,
event_tags: None,
event_properties: None,
api_base_url: String::new(),
string_redactor: None,
event_redactor: None,
log_destination: LogDestination::File,
}
}
}
impl Options {
#[must_use]
pub fn resolve_session_id<F, Fut>(mut self, hook: F) -> Self
where
F: Fn(ToolCall) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Option<String>, HookError>> + Send + 'static,
{
self.resolve_session_id = Some(Hook::new(hook));
self
}
#[must_use]
pub fn identify<F, Fut>(mut self, hook: F) -> Self
where
F: Fn(ToolCall) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Option<UserIdentity>, HookError>> + Send + 'static,
{
self.identify = Some(Hook::new(hook));
self
}
#[must_use]
pub fn event_tags<F, Fut>(mut self, hook: F) -> Self
where
F: Fn(ToolCall) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Option<Map<String, Value>>, HookError>> + Send + 'static,
{
self.event_tags = Some(Hook::new(hook));
self
}
#[must_use]
pub fn event_properties<F, Fut>(mut self, hook: F) -> Self
where
F: Fn(ToolCall) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Option<Map<String, Value>>, HookError>> + Send + 'static,
{
self.event_properties = Some(Hook::new(hook));
self
}
#[must_use]
pub fn api_base_url(mut self, url: impl Into<String>) -> Self {
self.api_base_url = url.into();
self
}
#[must_use]
pub fn redact_sensitive_information<F, Fut>(mut self, hook: F) -> Self
where
F: Fn(String) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<String, HookError>> + Send + 'static,
{
self.string_redactor = Some(Redactor::new(hook));
self
}
#[must_use]
pub fn redact_event<F, Fut>(mut self, hook: F) -> Self
where
F: Fn(Event) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Option<Event>, HookError>> + Send + 'static,
{
self.event_redactor = Some(Redactor::new(hook));
self
}
#[must_use]
pub fn log_destination(mut self, destination: LogDestination) -> Self {
self.log_destination = destination;
self
}
#[must_use]
pub fn enable_report_missing(mut self, on: bool) -> Self {
self.report_missing = on;
self
}
#[must_use]
pub fn enable_tracing(mut self, on: bool) -> Self {
self.tracing = on;
self
}
#[must_use]
pub fn enable_tool_call_context(mut self, on: bool) -> Self {
self.tool_call_context = on;
self
}
#[must_use]
pub fn enable_agent_tracking(mut self, on: bool) -> Self {
self.agent_tracking = on;
self
}
#[must_use]
pub fn custom_context_description(mut self, text: impl Into<String>) -> Self {
self.custom_context_description = text.into();
self
}
pub(crate) fn report_missing(&self) -> bool {
self.report_missing
}
pub(crate) fn tracing(&self) -> bool {
self.tracing
}
pub(crate) fn tool_call_context(&self) -> bool {
self.tool_call_context
}
pub(crate) fn agent_tracking(&self) -> bool {
self.agent_tracking
}
pub(crate) fn context_description(&self) -> &str {
if self.custom_context_description.is_empty() {
DEFAULT_CONTEXT_PARAMETER_DESCRIPTION
} else {
&self.custom_context_description
}
}
pub(crate) fn hook_mode(&self) -> bool {
self.resolve_session_id.is_some()
}
pub(crate) fn resolve_session_id_hook(&self) -> Option<&Hook<String>> {
self.resolve_session_id.as_ref()
}
pub(crate) fn identify_hook(&self) -> Option<&Hook<UserIdentity>> {
self.identify.as_ref()
}
pub(crate) fn event_tags_hook(&self) -> Option<&Hook<Map<String, Value>>> {
self.event_tags.as_ref()
}
pub(crate) fn event_properties_hook(&self) -> Option<&Hook<Map<String, Value>>> {
self.event_properties.as_ref()
}
pub(crate) fn api_base_url_option(&self) -> Option<&str> {
Some(self.api_base_url.as_str()).filter(|url| !url.is_empty())
}
pub(crate) fn string_redactor(&self) -> Option<&StringRedactor> {
self.string_redactor.as_ref()
}
pub(crate) fn event_redactor(&self) -> Option<&EventRedactor> {
self.event_redactor.as_ref()
}
pub(crate) fn log_to(&self) -> LogDestination {
self.log_destination
}
}