use serde_json::Value;
pub(crate) const FINAL_STATUS: &str = "/transcript_event/metadata/final_status";
pub(crate) const STOP_REASON: &str = "/transcript_event/metadata/stop_reason";
pub(crate) const TERMINAL_ERROR: &str = "/transcript_event/metadata/error";
pub(crate) const TERMINAL_CLASS: &str = "/transcript_event/metadata/terminal_class";
pub(crate) const MODEL: &str = "/transcript_event/metadata/model";
pub(crate) const PROVIDER: &str = "/transcript_event/metadata/provider";
pub(crate) const INPUT_TOKENS: &str = "/transcript_event/metadata/input_tokens";
pub(crate) const OUTPUT_TOKENS: &str = "/transcript_event/metadata/output_tokens";
pub(crate) const CACHE_READ_TOKENS: &str = "/transcript_event/metadata/cache_read_tokens";
pub(crate) const CACHE_WRITE_TOKENS: &str = "/transcript_event/metadata/cache_write_tokens";
pub(crate) const COST_USD: &str = "/transcript_event/metadata/cost_usd";
pub(crate) const PROVIDER_ATTEMPTS_TOTAL: &str =
"/transcript_event/metadata/provider_attempts/total";
pub(crate) const PROVIDER_ATTEMPTS_RATE_LIMITED: &str =
"/transcript_event/metadata/provider_attempts/rate_limited";
pub(crate) const PROVIDER_ATTEMPTS_EMPTY: &str =
"/transcript_event/metadata/provider_attempts/empty_completion";
pub(crate) const PROVIDER_ATTEMPTS_OTHER: &str =
"/transcript_event/metadata/provider_attempts/other";
pub(crate) const TOOL_CALL_ID: &str = "/transcript_event/metadata/tool_call_id";
pub(crate) const TOOL_NAME: &str = "/transcript_event/metadata/tool_name";
pub(crate) const TOOL_RAW_INPUT: &str = "/transcript_event/metadata/raw_input";
pub(crate) const TOOL_STATUS: &str = "/transcript_event/metadata/status";
pub(crate) const TOOL_DURATION_MS: &str = "/transcript_event/metadata/duration_ms";
pub(crate) const TOOL_IS_ERROR: &str = "/transcript_event/metadata/is_error";
pub(crate) const CHECKPOINT_KIND: &str = "/transcript_event/metadata/kind";
pub(crate) const ITERATION: &str = "/transcript_event/metadata/iteration";
pub(crate) const TEXT: [&str; 2] = ["/transcript_event/text", "/raw_message/content"];
pub(crate) const ROLE: [&str; 2] = ["/transcript_event/role", "/raw_message/role"];
pub(crate) const TOOL_NAME_ANY: [&str; 3] = [
TOOL_NAME,
"/raw_message/name",
"/raw_message/tool_calls/0/name",
];
pub(crate) const TOOL_INPUT_ANY: [&str; 3] = [
"/transcript_event/metadata/input",
"/raw_message/input",
"/raw_message/tool_calls/0/arguments",
];
pub(crate) const TOOL_OUTPUT_ANY: [&str; 3] = [
"/transcript_event/metadata/output",
"/raw_message/content",
"/transcript_event/text",
];
pub(crate) fn semantic_string(payload: &Value, pointers: &[&str]) -> Option<String> {
pointers.iter().find_map(|pointer| {
payload
.pointer(pointer)
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
})
}
pub(crate) fn semantic_value(payload: &Value, pointers: &[&str]) -> Option<Value> {
pointers
.iter()
.find_map(|pointer| payload.pointer(pointer))
.cloned()
}
pub(crate) fn string_at(payload: &Value, pointer: &str) -> Option<String> {
semantic_string(payload, &[pointer])
}
pub(crate) fn i64_at(payload: &Value, pointer: &str) -> Option<i64> {
let value = payload.pointer(pointer)?;
value
.as_i64()
.or_else(|| value.as_f64().map(|number| number as i64))
}
pub(crate) fn f64_at(payload: &Value, pointer: &str) -> Option<f64> {
payload.pointer(pointer).and_then(Value::as_f64)
}
pub(crate) fn bool_at(payload: &Value, pointer: &str) -> bool {
payload
.pointer(pointer)
.and_then(Value::as_bool)
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
fn llm_call_payload() -> Value {
serde_json::json!({
"transcript_event": {
"blocks": [{"text": "LLM call completed", "type": "text"}],
"id": "019fc7e6-71f4-7583-8664-55fdb52f962e",
"kind": "llm_call",
"metadata": {
"cache_read_tokens": 0,
"cache_write_tokens": 10948,
"canonical_stop_reason": "end_turn",
"cost_usd": 0.002872,
"input_tokens": 10951,
"model": "gpt-5.6-luna",
"output_tokens": 112,
"provider": "openai",
"provider_stop_reason": "completed"
},
"role": "assistant",
"text": "LLM call completed"
}
})
}
#[test]
fn typed_accessors_read_a_real_llm_call_payload() {
let payload = llm_call_payload();
assert_eq!(string_at(&payload, MODEL).as_deref(), Some("gpt-5.6-luna"));
assert_eq!(string_at(&payload, PROVIDER).as_deref(), Some("openai"));
assert_eq!(i64_at(&payload, INPUT_TOKENS), Some(10951));
assert_eq!(i64_at(&payload, OUTPUT_TOKENS), Some(112));
assert_eq!(i64_at(&payload, CACHE_WRITE_TOKENS), Some(10948));
assert_eq!(f64_at(&payload, COST_USD), Some(0.002872));
assert_eq!(
semantic_string(&payload, &TEXT).as_deref(),
Some("LLM call completed")
);
assert_eq!(
semantic_string(&payload, &ROLE).as_deref(),
Some("assistant")
);
}
#[test]
fn absent_and_blank_facts_read_as_missing_rather_than_empty() {
let payload = serde_json::json!({
"transcript_event": {"text": " ", "metadata": {"model": ""}}
});
assert_eq!(string_at(&payload, MODEL), None);
assert_eq!(semantic_string(&payload, &TEXT), None);
assert_eq!(i64_at(&payload, INPUT_TOKENS), None);
assert_eq!(f64_at(&payload, COST_USD), None);
assert!(!bool_at(&payload, TOOL_IS_ERROR));
}
#[test]
fn token_counts_serialized_as_floats_still_read_as_integers() {
let payload = serde_json::json!({
"transcript_event": {"metadata": {"input_tokens": 1024.0}}
});
assert_eq!(i64_at(&payload, INPUT_TOKENS), Some(1024));
}
#[test]
fn text_prefers_the_transcript_envelope_over_the_raw_message() {
let payload = serde_json::json!({
"transcript_event": {"text": "envelope"},
"raw_message": {"content": "raw"}
});
assert_eq!(
semantic_string(&payload, &TEXT).as_deref(),
Some("envelope")
);
let raw_only = serde_json::json!({"raw_message": {"content": "raw"}});
assert_eq!(semantic_string(&raw_only, &TEXT).as_deref(), Some("raw"));
}
}