Skip to main content

agentcat_sdk/
config.rs

1//! [`Options`]: what `AgentCat` adds to a server's tools and what it records.
2
3use 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
18/// The error a hook returns. The SDK logs it and goes on without what the hook would have given.
19pub type HookError = Box<dyn std::error::Error + Send + Sync + 'static>;
20
21/// Who calls the tools, as the `identify` hook reports it.
22///
23/// ```
24/// use agentcat_sdk::UserIdentity;
25///
26/// let user = UserIdentity::new("user-42").user_name("Ada");
27/// # let _ = user;
28/// ```
29#[derive(Clone, Debug, PartialEq)]
30#[non_exhaustive]
31pub struct UserIdentity {
32    /// The caller's id in the customer's system.
33    pub user_id: String,
34    /// The caller's display name.
35    pub user_name: Option<String>,
36    /// Any other data about the caller.
37    pub user_data: Option<Map<String, Value>>,
38}
39
40impl UserIdentity {
41    /// An identity with only the id.
42    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    /// Sets the display name.
51    #[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    /// Sets the other data about the caller.
58    #[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
65/// The future a hook returns.
66pub(crate) type HookFuture<T> = Pin<Box<dyn Future<Output = Result<Option<T>, HookError>> + Send>>;
67
68/// A customer hook, shared between the clones of the [`Options`] that hold it.
69pub(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    /// The future of one call of the hook. The hook's own code, including a closure body that is
81    /// not `async`, runs when the future is first polled, not here.
82    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
100/// The future a redaction hook returns.
101pub(crate) type RedactFuture<T> = Pin<Box<dyn Future<Output = Result<T, HookError>> + Send>>;
102
103/// A customer redaction hook from `A` to `T`, shared between the clones of the [`Options`] that
104/// hold it.
105pub(crate) struct Redactor<A, T>(Arc<dyn Fn(A) -> RedactFuture<T> + Send + Sync>);
106
107/// The hook of [`Options::redact_sensitive_information`].
108pub(crate) type StringRedactor = Redactor<String, String>;
109
110/// The hook of [`Options::redact_event`].
111pub(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    /// The future of one call of the hook. The hook's own code runs when the future is first
123    /// polled, not here.
124    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/// Where the SDK writes its own log lines. The destination is process-wide: the
147/// first handle built decides it.
148#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
149#[non_exhaustive]
150pub enum LogDestination {
151    /// The file `agentcat.log` in the user's home directory, one line per message.
152    #[default]
153    File,
154    /// `tracing` events with the target `agentcat`.
155    ///
156    /// The delivery thread of each handle emits these events, not the thread of the tool call. A
157    /// subscriber that `tracing::subscriber::with_default` sets on another thread does not see
158    /// them. Set a global subscriber instead, for example with
159    /// `tracing::subscriber::set_global_default`.
160    Tracing,
161}
162
163/// Settings for an [`AgentCat`](crate::AgentCat) handle. [`Options::default`] turns on the
164/// `context` parameter, tracing and the `get_more_tools` tool, and turns off agent tracking; each
165/// setter consumes the options and returns them changed.
166///
167/// ```
168/// use agentcat_sdk::Options;
169///
170/// let options = Options::default()
171///     .enable_agent_tracking(true)
172///     .custom_context_description("Say why you call this tool.");
173/// # let _ = options;
174/// ```
175///
176/// The four hooks (`resolve_session_id`, `identify`, `event_tags`, `event_properties`) are async
177/// closures over a [`ToolCall`]. The SDK starts them on the tokio runtime that serves the call and
178/// never waits for them, so a slow hook cannot delay a tool call. A hook that returns an error or
179/// `None` gives nothing. `rmcp`'s `local` feature is not supported, because hooks run as `Send`
180/// tasks.
181#[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    /// The defaults: missing-tool reports on, tracing on, the `context` parameter
204    /// on, agent tracking off, and the default `context` description.
205    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    /// Decides the session of each tool call (hook mode). With this hook set, tools get no
226    /// `session_id` parameter and the SDK mints nothing back to the agent. The hook returns the
227    /// session id, or `None` to let the SDK mint one.
228    #[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    /// Says who made each tool call. The hook runs on every call; `None` leaves the call
239    /// anonymous.
240    #[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    /// Adds the returned tags to the event of each tool call.
251    #[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    /// Adds the returned properties to the event of each tool call.
262    #[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    /// The base URL of the `AgentCat` API, in place of the environment's and the default. An empty
273    /// text means none is set. The URL is used as given.
274    #[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    /// Redacts every string of each event before it leaves the process: the hook gets one string
281    /// at a time and returns the string to send. It runs on the tokio runtime that serves the
282    /// call. When it fails, the SDK drops the event rather than send it unredacted.
283    ///
284    /// The SDK gives the hook no time limit. An event holds one
285    /// of the 5 delivery slots of its handle while the hook runs on it. A hook that never
286    /// completes holds its slot for good, and five such hooks stop the delivery of the handle:
287    /// later events wait in the queue until a shutdown abandons them.
288    #[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    /// Changes each event before it leaves the process: the hook gets the whole [`Event`] and
299    /// returns the event to send, or `Ok(None)` to drop it. It runs on the tokio runtime that
300    /// serves the call. When it fails, the SDK drops the event. To unit-test the hook, call it
301    /// with an event from [`Event::default`].
302    ///
303    /// The SDK gives the hook no time limit. An event holds one
304    /// of the 5 delivery slots of its handle while the hook runs on it. A hook that never
305    /// completes holds its slot for good, and five such hooks stop the delivery of the handle:
306    /// later events wait in the queue until a shutdown abandons them.
307    #[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    /// Where the SDK writes its own log lines. The first handle built in the process decides. The
318    /// default is [`LogDestination::File`].
319    #[must_use]
320    pub fn log_destination(mut self, destination: LogDestination) -> Self {
321        self.log_destination = destination;
322        self
323    }
324
325    /// Whether tool listings get the `get_more_tools` tool, through which agents report a tool
326    /// they miss.
327    #[must_use]
328    pub fn enable_report_missing(mut self, on: bool) -> Self {
329        self.report_missing = on;
330        self
331    }
332
333    /// Whether tool calls are recorded and tools get the `session_id` parameter. With tracing
334    /// off, tools get neither `session_id` nor `agent_id` (also with agent tracking on), and
335    /// nothing is recorded; tools still get the `context` parameter and `get_more_tools`. Each
336    /// call goes to the tool without the parameters the SDK added to it, as with tracing on, so a
337    /// tool that declares its own `context` keeps it.
338    #[must_use]
339    pub fn enable_tracing(mut self, on: bool) -> Self {
340        self.tracing = on;
341        self
342    }
343
344    /// Whether tools get a required `context` parameter, in which agents say why they call the
345    /// tool. The text is recorded as the intent of the call.
346    #[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    /// Whether tools get a required `agent_id` parameter, with which agents identify themselves.
353    /// Only with tracing on ([`enable_tracing`](Self::enable_tracing)).
354    #[must_use]
355    pub fn enable_agent_tracking(mut self, on: bool) -> Self {
356        self.agent_tracking = on;
357        self
358    }
359
360    /// The description of the `context` parameter that agents read. An empty text means the
361    /// default description.
362    #[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    /// Whether tool listings get `get_more_tools`.
369    pub(crate) fn report_missing(&self) -> bool {
370        self.report_missing
371    }
372
373    /// Whether tool calls are recorded and handles injected.
374    pub(crate) fn tracing(&self) -> bool {
375        self.tracing
376    }
377
378    /// Whether tools get the `context` parameter.
379    pub(crate) fn tool_call_context(&self) -> bool {
380        self.tool_call_context
381    }
382
383    /// Whether tools get the `agent_id` parameter.
384    pub(crate) fn agent_tracking(&self) -> bool {
385        self.agent_tracking
386    }
387
388    /// The description of the `context` parameter: the custom text, or the default where it is
389    /// empty.
390    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    /// Whether a `resolve_session_id` hook decides the session ("hook mode").
399    pub(crate) fn hook_mode(&self) -> bool {
400        self.resolve_session_id.is_some()
401    }
402
403    /// The `resolve_session_id` hook.
404    pub(crate) fn resolve_session_id_hook(&self) -> Option<&Hook<String>> {
405        self.resolve_session_id.as_ref()
406    }
407
408    /// The `identify` hook.
409    pub(crate) fn identify_hook(&self) -> Option<&Hook<UserIdentity>> {
410        self.identify.as_ref()
411    }
412
413    /// The `event_tags` hook.
414    pub(crate) fn event_tags_hook(&self) -> Option<&Hook<Map<String, Value>>> {
415        self.event_tags.as_ref()
416    }
417
418    /// The `event_properties` hook.
419    pub(crate) fn event_properties_hook(&self) -> Option<&Hook<Map<String, Value>>> {
420        self.event_properties.as_ref()
421    }
422
423    /// The base URL set with [`api_base_url`](Self::api_base_url), when it is not empty.
424    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    /// The `redact_sensitive_information` hook.
429    pub(crate) fn string_redactor(&self) -> Option<&StringRedactor> {
430        self.string_redactor.as_ref()
431    }
432
433    /// The `redact_event` hook.
434    pub(crate) fn event_redactor(&self) -> Option<&EventRedactor> {
435        self.event_redactor.as_ref()
436    }
437
438    /// Where the SDK writes its log lines.
439    pub(crate) fn log_to(&self) -> LogDestination {
440        self.log_destination
441    }
442}