Skip to main content

agentcat_sdk/event/
public.rs

1//! [`Event`]: the event a `redact_event` hook sees and returns, typed, and its conversion from and
2//! into the pipeline's [`EventObject`].
3use std::collections::BTreeMap;
4use std::time::{Duration, SystemTime, UNIX_EPOCH};
5
6use serde_json::{Map, Value};
7
8use crate::pipeline::EventObject;
9use crate::wire;
10
11/// An event as `AgentCat` publishes it, as the hook of
12/// [`Options::redact_event`](crate::Options::redact_event) sees it. The hook may change any
13/// field. It cannot add a field that events do not have, and the SDK restores the system fields
14/// (`id`, `session_id`, `project_id`, `event_type` and `timestamp`) when the hook changes them.
15///
16/// The free-form fields (`parameters`, `response`, `user_intent`, `error`, `identify_actor_data`
17/// and `properties`) hold JSON values, as the published event carries them.
18///
19/// To unit-test a `redact_event` hook, build an event with [`Event::default`], set the fields
20/// the hook reads, and call the hook with it.
21#[derive(Clone, Debug, PartialEq)]
22#[non_exhaustive]
23pub struct Event {
24    /// The ID of the event.
25    pub id: String,
26    /// The session the event belongs to.
27    pub session_id: Option<String>,
28    /// The project the event is published to.
29    pub project_id: String,
30    /// The kind of event, such as `mcp:tools/call`.
31    pub event_type: String,
32    /// When the event happened.
33    pub timestamp: SystemTime,
34    /// How long it took.
35    pub duration: Option<Duration>,
36    /// The estimated tokens of the request.
37    pub input_tokens: Option<u32>,
38    /// The estimated tokens of the response.
39    pub output_tokens: Option<u32>,
40    /// The language of the SDK: `"Rust"`.
41    pub sdk_language: Option<String>,
42    /// The version of the SDK.
43    pub agentcat_version: Option<String>,
44    /// The name of the MCP server.
45    pub server_name: Option<String>,
46    /// The version of the MCP server.
47    pub server_version: Option<String>,
48    /// The name of the MCP client.
49    pub client_name: Option<String>,
50    /// The version of the MCP client.
51    pub client_version: Option<String>,
52    /// The ID of the caller in the customer's system.
53    pub identify_actor_given_id: Option<String>,
54    /// The display name of the caller.
55    pub identify_actor_name: Option<String>,
56    /// Other data about the caller.
57    pub identify_actor_data: Option<Map<String, Value>>,
58    /// The name of the tool or resource.
59    pub resource_name: Option<String>,
60    /// The parameters of the call.
61    pub parameters: Option<Value>,
62    /// The response of the call.
63    pub response: Option<Value>,
64    /// Why the agent made the call.
65    pub user_intent: Option<Value>,
66    /// Whether the call failed.
67    pub is_error: Option<bool>,
68    /// The error of the call.
69    pub error: Option<Value>,
70    /// The tags of the event.
71    pub tags: Option<BTreeMap<String, String>>,
72    /// The properties of the event.
73    pub properties: Option<Map<String, Value>>,
74}
75
76impl Default for Event {
77    /// An empty event: `id`, `project_id` and `event_type` are empty, `timestamp` is the Unix
78    /// epoch, and every other field is `None`. Set the fields a test needs:
79    ///
80    /// ```
81    /// use agentcat_sdk::Event;
82    /// use serde_json::json;
83    ///
84    /// let mut event = Event::default();
85    /// event.parameters = Some(json!({"api_key": "fake-secret-42"}));
86    /// # assert!(event.response.is_none());
87    /// ```
88    fn default() -> Self {
89        Self {
90            id: String::new(),
91            session_id: None,
92            project_id: String::new(),
93            event_type: String::new(),
94            timestamp: UNIX_EPOCH,
95            duration: None,
96            input_tokens: None,
97            output_tokens: None,
98            sdk_language: None,
99            agentcat_version: None,
100            server_name: None,
101            server_version: None,
102            client_name: None,
103            client_version: None,
104            identify_actor_given_id: None,
105            identify_actor_name: None,
106            identify_actor_data: None,
107            resource_name: None,
108            parameters: None,
109            response: None,
110            user_intent: None,
111            is_error: None,
112            error: None,
113            tags: None,
114            properties: None,
115        }
116    }
117}
118
119impl Event {
120    /// The typed event of `event`. Keys the type has no field for are left out; a field of the
121    /// wrong type is `None`; a `timestamp` that does not parse is the Unix epoch, which the
122    /// restore of `timestamp` after the hook makes harmless.
123    pub(crate) fn from_object(event: &EventObject) -> Event {
124        let text = |key: &str| event.get(key).and_then(Value::as_str).map(str::to_owned);
125        let object = |key: &str| event.get(key).and_then(Value::as_object).cloned();
126        let value = |key: &str| event.get(key).cloned();
127        let tokens = |key: &str| {
128            event
129                .get(key)
130                .and_then(Value::as_u64)
131                .and_then(|n| u32::try_from(n).ok())
132        };
133        Event {
134            id: text("id").unwrap_or_default(),
135            session_id: text("sessionId").filter(|id| !id.is_empty()),
136            project_id: text("projectId").unwrap_or_default(),
137            event_type: text("eventType").unwrap_or_default(),
138            timestamp: event
139                .get("timestamp")
140                .and_then(Value::as_str)
141                .and_then(wire::parse_iso_millis)
142                .unwrap_or(UNIX_EPOCH),
143            duration: event
144                .get("duration")
145                .and_then(Value::as_u64)
146                .map(Duration::from_millis),
147            input_tokens: tokens("inputTokens"),
148            output_tokens: tokens("outputTokens"),
149            sdk_language: text("sdkLanguage"),
150            agentcat_version: text("agentcatVersion"),
151            server_name: text("serverName"),
152            server_version: text("serverVersion"),
153            client_name: text("clientName"),
154            client_version: text("clientVersion"),
155            identify_actor_given_id: text("identifyActorGivenId"),
156            identify_actor_name: text("identifyActorName"),
157            identify_actor_data: object("identifyActorData"),
158            resource_name: text("resourceName"),
159            parameters: value("parameters"),
160            response: value("response"),
161            user_intent: value("userIntent"),
162            is_error: event.get("isError").and_then(Value::as_bool),
163            error: value("error"),
164            tags: object("tags").map(|tags| {
165                tags.into_iter()
166                    .filter_map(|(key, tag)| Some((key, tag.as_str()?.to_owned())))
167                    .collect()
168            }),
169            properties: object("properties"),
170        }
171    }
172
173    /// The pipeline's event, with the keys in the order of a tool-call event (`id`,
174    /// `sessionId`, `projectId`, `eventType`, `timestamp`, `duration`, ...). A field that is
175    /// `None` is absent, except `sessionId`, which is `""` without a session. Custom events with
176    /// a session string skip the redaction hooks, so only this order is needed.
177    pub(crate) fn into_object(self) -> EventObject {
178        let mut out = EventObject::new();
179        let mut put = |key: &str, value: Option<Value>| {
180            if let Some(value) = value {
181                out.insert(key.to_owned(), value);
182            }
183        };
184        let text = |value: Option<String>| value.map(Value::String);
185        put("id", Some(Value::String(self.id)));
186        put(
187            "sessionId",
188            Some(Value::String(self.session_id.unwrap_or_default())),
189        );
190        put("projectId", Some(Value::String(self.project_id)));
191        put("eventType", Some(Value::String(self.event_type)));
192        put(
193            "timestamp",
194            Some(Value::String(wire::iso_millis(self.timestamp))),
195        );
196        put(
197            "duration",
198            self.duration
199                .and_then(|d| u64::try_from(d.as_millis()).ok())
200                .map(Value::from),
201        );
202        put("inputTokens", self.input_tokens.map(Value::from));
203        put("outputTokens", self.output_tokens.map(Value::from));
204        put("sdkLanguage", text(self.sdk_language));
205        put("agentcatVersion", text(self.agentcat_version));
206        put("serverName", text(self.server_name));
207        put("serverVersion", text(self.server_version));
208        put("clientName", text(self.client_name));
209        put("clientVersion", text(self.client_version));
210        put("identifyActorGivenId", text(self.identify_actor_given_id));
211        put("identifyActorName", text(self.identify_actor_name));
212        put(
213            "identifyActorData",
214            self.identify_actor_data.map(Value::Object),
215        );
216        put("resourceName", text(self.resource_name));
217        put("parameters", self.parameters);
218        put("response", self.response);
219        put("userIntent", self.user_intent);
220        put("isError", self.is_error.map(Value::Bool));
221        put("error", self.error);
222        put(
223            "tags",
224            self.tags.map(|tags| {
225                Value::Object(
226                    tags.into_iter()
227                        .map(|(key, tag)| (key, Value::String(tag)))
228                        .collect(),
229                )
230            }),
231        );
232        put("properties", self.properties.map(Value::Object));
233        out
234    }
235}