use std::backtrace::{Backtrace, BacktraceStatus};
use std::fmt;
use serde_json::{Map, Value};
use tokio::runtime::Handle;
use super::{CallRecord, redact_extra};
use crate::hooks::PendingHooks;
use crate::pipeline::EventObject;
use crate::wire;
#[cfg(test)]
mod tests;
const TOOL_CALL_EVENT_TYPE: &str = "mcp:tools/call";
pub(crate) const SDK_LANGUAGE: &str = "Rust";
const RESULT_TYPE_KEY: &str = "resultType";
const RESULT_TYPE_COMPLETE: &str = "complete";
pub(crate) struct EventJob {
pub(crate) event: EventObject,
pub(crate) hooks: PendingHooks,
pub(crate) hook_mode: bool,
pub(crate) runtime: Option<Handle>,
pub(crate) string_redaction: bool,
pub(crate) event_redaction: bool,
pub(crate) backtrace: Option<Backtrace>,
}
impl fmt::Debug for EventJob {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("EventJob")
.field("event", &debug_redacted(&self.event))
.field("hooks", &self.hooks)
.field("hook_mode", &self.hook_mode)
.field("runtime", &self.runtime.is_some())
.field("string_redaction", &self.string_redaction)
.field("event_redaction", &self.event_redaction)
.field("backtrace", &self.backtrace.as_ref().map(Backtrace::status))
.finish()
}
}
fn debug_redacted(event: &EventObject) -> EventObject {
let mut event = event.clone();
if let Some(extra) = event.get_mut("parameters").and_then(|p| p.get_mut("extra")) {
*extra = redact_extra(extra);
}
event
}
pub(crate) fn from_call(record: CallRecord, project_id: &str) -> EventJob {
let CallRecord {
started_at,
duration_ms,
resolution,
resource_name,
request,
extra,
input_tokens,
output_tokens,
tags,
user_intent,
response,
is_error,
error,
server_name,
server_version,
client,
pending,
runtime,
backtrace,
} = record;
let (client_name, client_version) =
client.map_or((None, None), |client| (client.name, client.version));
let text = |value: &str| Value::String(value.to_owned());
let mut event = EventObject::new();
let mut put = |key: &str, value: Option<Value>| {
if let Some(value) = value {
event.insert(key.to_owned(), value);
}
};
put("id", Some(text("")));
put("sessionId", Some(Value::String(resolution.session_id)));
put("projectId", Some(text(project_id)));
put("eventType", Some(text(TOOL_CALL_EVENT_TYPE)));
put(
"timestamp",
Some(Value::String(wire::iso_millis(started_at))),
);
put("duration", Some(Value::from(duration_ms)));
put("inputTokens", input_tokens.map(Value::from));
put("outputTokens", output_tokens.map(Value::from));
put("sdkLanguage", Some(text(SDK_LANGUAGE)));
put("agentcatVersion", Some(text(env!("CARGO_PKG_VERSION"))));
put("serverName", server_name.map(Value::String));
put("serverVersion", server_version.map(Value::String));
put("clientName", client_name.map(Value::String));
put("clientVersion", client_version.map(Value::String));
put("identifyActorData", Some(Value::Object(Map::new())));
put("resourceName", Some(Value::String(resource_name)));
let mut parameters = Map::new();
parameters.insert("request".to_owned(), request);
parameters.insert("extra".to_owned(), extra);
put("parameters", Some(Value::Object(parameters)));
put("response", response.map(without_complete_type));
put("userIntent", user_intent);
put("isError", is_error.then_some(Value::Bool(true)));
put("error", error.map(Value::Object));
put("tags", Some(Value::Object(tags)));
EventJob {
event,
hooks: pending,
hook_mode: resolution.hook_mode,
runtime,
string_redaction: true,
event_redaction: true,
backtrace,
}
}
fn without_complete_type(response: Value) -> Value {
match response {
Value::Object(mut fields) => {
if fields.get(RESULT_TYPE_KEY).and_then(Value::as_str) == Some(RESULT_TYPE_COMPLETE) {
fields.shift_remove(RESULT_TYPE_KEY);
}
Value::Object(fields)
}
other => other,
}
}
pub(crate) fn render_backtrace(event: &mut EventObject, backtrace: &Backtrace) {
if backtrace.status() != BacktraceStatus::Captured {
return;
}
let Some(Value::Object(error)) = event.get_mut("error") else {
return;
};
let index_of = |name: &str| error.keys().position(|key| key == name);
let index = index_of("platform")
.map(|platform| platform + 1)
.or_else(|| index_of("chained_errors"))
.unwrap_or(error.len());
error.shift_insert(
index,
"stack".to_owned(),
Value::String(backtrace.to_string()),
);
}