1use std::fmt;
4use std::future::Future;
5use std::pin::Pin;
6use std::sync::Arc;
7
8use serde_json::{Map, Value};
9
10use crate::constants::DEFAULT_CONTEXT_PARAMETER_DESCRIPTION;
11use crate::event::Event;
12use crate::hooks::ToolCall;
13
14pub(crate) mod env;
15#[cfg(test)]
16mod tests;
17
18pub type HookError = Box<dyn std::error::Error + Send + Sync + 'static>;
20
21#[derive(Clone, Debug, PartialEq)]
30#[non_exhaustive]
31pub struct UserIdentity {
32 pub user_id: String,
34 pub user_name: Option<String>,
36 pub user_data: Option<Map<String, Value>>,
38}
39
40impl UserIdentity {
41 pub fn new(user_id: impl Into<String>) -> Self {
43 Self {
44 user_id: user_id.into(),
45 user_name: None,
46 user_data: None,
47 }
48 }
49
50 #[must_use]
52 pub fn user_name(mut self, name: impl Into<String>) -> Self {
53 self.user_name = Some(name.into());
54 self
55 }
56
57 #[must_use]
59 pub fn user_data(mut self, data: Map<String, Value>) -> Self {
60 self.user_data = Some(data);
61 self
62 }
63}
64
65pub(crate) type HookFuture<T> = Pin<Box<dyn Future<Output = Result<Option<T>, HookError>> + Send>>;
67
68pub(crate) struct Hook<T>(Arc<dyn Fn(ToolCall) -> HookFuture<T> + Send + Sync>);
70
71impl<T: 'static> Hook<T> {
72 fn new<F, Fut>(hook: F) -> Self
73 where
74 F: Fn(ToolCall) -> Fut + Send + Sync + 'static,
75 Fut: Future<Output = Result<Option<T>, HookError>> + Send + 'static,
76 {
77 Self(Arc::new(move |call| Box::pin(hook(call))))
78 }
79
80 pub(crate) fn call(&self, call: ToolCall) -> HookFuture<T> {
83 let hook = Arc::clone(&self.0);
84 Box::pin(async move { hook(call).await })
85 }
86}
87
88impl<T> Clone for Hook<T> {
89 fn clone(&self) -> Self {
90 Self(Arc::clone(&self.0))
91 }
92}
93
94impl<T> fmt::Debug for Hook<T> {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 fmt::Debug::fmt("<hook>", f)
97 }
98}
99
100pub(crate) type RedactFuture<T> = Pin<Box<dyn Future<Output = Result<T, HookError>> + Send>>;
102
103pub(crate) struct Redactor<A, T>(Arc<dyn Fn(A) -> RedactFuture<T> + Send + Sync>);
106
107pub(crate) type StringRedactor = Redactor<String, String>;
109
110pub(crate) type EventRedactor = Redactor<Event, Option<Event>>;
112
113impl<A, T> Redactor<A, T> {
114 fn new<F, Fut>(hook: F) -> Self
115 where
116 F: Fn(A) -> Fut + Send + Sync + 'static,
117 Fut: Future<Output = Result<T, HookError>> + Send + 'static,
118 {
119 Self(Arc::new(move |input| Box::pin(hook(input))))
120 }
121
122 pub(crate) fn call(&self, input: A) -> RedactFuture<T>
125 where
126 A: Send + 'static,
127 T: 'static,
128 {
129 let hook = Arc::clone(&self.0);
130 Box::pin(async move { hook(input).await })
131 }
132}
133
134impl<A, T> Clone for Redactor<A, T> {
135 fn clone(&self) -> Self {
136 Self(Arc::clone(&self.0))
137 }
138}
139
140impl<A, T> fmt::Debug for Redactor<A, T> {
141 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142 fmt::Debug::fmt("<hook>", f)
143 }
144}
145
146#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
149#[non_exhaustive]
150pub enum LogDestination {
151 #[default]
153 File,
154 Tracing,
161}
162
163#[derive(Clone, Debug)]
182#[expect(
183 clippy::struct_excessive_bools,
184 reason = "the four boolean switches of the options"
185)]
186pub struct Options {
187 report_missing: bool,
188 tracing: bool,
189 tool_call_context: bool,
190 agent_tracking: bool,
191 custom_context_description: String,
192 resolve_session_id: Option<Hook<String>>,
193 identify: Option<Hook<UserIdentity>>,
194 event_tags: Option<Hook<Map<String, Value>>>,
195 event_properties: Option<Hook<Map<String, Value>>>,
196 api_base_url: String,
197 string_redactor: Option<StringRedactor>,
198 event_redactor: Option<EventRedactor>,
199 log_destination: LogDestination,
200}
201
202impl Default for Options {
203 fn default() -> Self {
206 Self {
207 report_missing: true,
208 tracing: true,
209 tool_call_context: true,
210 agent_tracking: false,
211 custom_context_description: String::new(),
212 resolve_session_id: None,
213 identify: None,
214 event_tags: None,
215 event_properties: None,
216 api_base_url: String::new(),
217 string_redactor: None,
218 event_redactor: None,
219 log_destination: LogDestination::File,
220 }
221 }
222}
223
224impl Options {
225 #[must_use]
229 pub fn resolve_session_id<F, Fut>(mut self, hook: F) -> Self
230 where
231 F: Fn(ToolCall) -> Fut + Send + Sync + 'static,
232 Fut: Future<Output = Result<Option<String>, HookError>> + Send + 'static,
233 {
234 self.resolve_session_id = Some(Hook::new(hook));
235 self
236 }
237
238 #[must_use]
241 pub fn identify<F, Fut>(mut self, hook: F) -> Self
242 where
243 F: Fn(ToolCall) -> Fut + Send + Sync + 'static,
244 Fut: Future<Output = Result<Option<UserIdentity>, HookError>> + Send + 'static,
245 {
246 self.identify = Some(Hook::new(hook));
247 self
248 }
249
250 #[must_use]
252 pub fn event_tags<F, Fut>(mut self, hook: F) -> Self
253 where
254 F: Fn(ToolCall) -> Fut + Send + Sync + 'static,
255 Fut: Future<Output = Result<Option<Map<String, Value>>, HookError>> + Send + 'static,
256 {
257 self.event_tags = Some(Hook::new(hook));
258 self
259 }
260
261 #[must_use]
263 pub fn event_properties<F, Fut>(mut self, hook: F) -> Self
264 where
265 F: Fn(ToolCall) -> Fut + Send + Sync + 'static,
266 Fut: Future<Output = Result<Option<Map<String, Value>>, HookError>> + Send + 'static,
267 {
268 self.event_properties = Some(Hook::new(hook));
269 self
270 }
271
272 #[must_use]
275 pub fn api_base_url(mut self, url: impl Into<String>) -> Self {
276 self.api_base_url = url.into();
277 self
278 }
279
280 #[must_use]
289 pub fn redact_sensitive_information<F, Fut>(mut self, hook: F) -> Self
290 where
291 F: Fn(String) -> Fut + Send + Sync + 'static,
292 Fut: Future<Output = Result<String, HookError>> + Send + 'static,
293 {
294 self.string_redactor = Some(Redactor::new(hook));
295 self
296 }
297
298 #[must_use]
308 pub fn redact_event<F, Fut>(mut self, hook: F) -> Self
309 where
310 F: Fn(Event) -> Fut + Send + Sync + 'static,
311 Fut: Future<Output = Result<Option<Event>, HookError>> + Send + 'static,
312 {
313 self.event_redactor = Some(Redactor::new(hook));
314 self
315 }
316
317 #[must_use]
320 pub fn log_destination(mut self, destination: LogDestination) -> Self {
321 self.log_destination = destination;
322 self
323 }
324
325 #[must_use]
328 pub fn enable_report_missing(mut self, on: bool) -> Self {
329 self.report_missing = on;
330 self
331 }
332
333 #[must_use]
339 pub fn enable_tracing(mut self, on: bool) -> Self {
340 self.tracing = on;
341 self
342 }
343
344 #[must_use]
347 pub fn enable_tool_call_context(mut self, on: bool) -> Self {
348 self.tool_call_context = on;
349 self
350 }
351
352 #[must_use]
355 pub fn enable_agent_tracking(mut self, on: bool) -> Self {
356 self.agent_tracking = on;
357 self
358 }
359
360 #[must_use]
363 pub fn custom_context_description(mut self, text: impl Into<String>) -> Self {
364 self.custom_context_description = text.into();
365 self
366 }
367
368 pub(crate) fn report_missing(&self) -> bool {
370 self.report_missing
371 }
372
373 pub(crate) fn tracing(&self) -> bool {
375 self.tracing
376 }
377
378 pub(crate) fn tool_call_context(&self) -> bool {
380 self.tool_call_context
381 }
382
383 pub(crate) fn agent_tracking(&self) -> bool {
385 self.agent_tracking
386 }
387
388 pub(crate) fn context_description(&self) -> &str {
391 if self.custom_context_description.is_empty() {
392 DEFAULT_CONTEXT_PARAMETER_DESCRIPTION
393 } else {
394 &self.custom_context_description
395 }
396 }
397
398 pub(crate) fn hook_mode(&self) -> bool {
400 self.resolve_session_id.is_some()
401 }
402
403 pub(crate) fn resolve_session_id_hook(&self) -> Option<&Hook<String>> {
405 self.resolve_session_id.as_ref()
406 }
407
408 pub(crate) fn identify_hook(&self) -> Option<&Hook<UserIdentity>> {
410 self.identify.as_ref()
411 }
412
413 pub(crate) fn event_tags_hook(&self) -> Option<&Hook<Map<String, Value>>> {
415 self.event_tags.as_ref()
416 }
417
418 pub(crate) fn event_properties_hook(&self) -> Option<&Hook<Map<String, Value>>> {
420 self.event_properties.as_ref()
421 }
422
423 pub(crate) fn api_base_url_option(&self) -> Option<&str> {
425 Some(self.api_base_url.as_str()).filter(|url| !url.is_empty())
426 }
427
428 pub(crate) fn string_redactor(&self) -> Option<&StringRedactor> {
430 self.string_redactor.as_ref()
431 }
432
433 pub(crate) fn event_redactor(&self) -> Option<&EventRedactor> {
435 self.event_redactor.as_ref()
436 }
437
438 pub(crate) fn log_to(&self) -> LogDestination {
440 self.log_destination
441 }
442}