Build a `Provenance` entry for an agent turn. Populates vendor, model, tool, suggestion type, session ID, and prompt hash. Token usage and cost can be added later when that data is available from the agent's transcript.
(options: &TurnRecordOptions<'_>)
| 24 | /// Token usage and cost can be added later when that data is available from |
| 25 | /// the agent's transcript. |
| 26 | pub(crate) fn build_turn_provenance(options: &TurnRecordOptions<'_>) -> Provenance { |
| 27 | let vendor = if options.session.agent_vendor.is_empty() { |
| 28 | vendor_from_agent_name(&options.session.agent_name) |
| 29 | } else { |
| 30 | AIVendor::parse(&options.session.agent_vendor) |
| 31 | }; |
| 32 | |
| 33 | let model = if options.session.model.is_empty() { |
| 34 | "unknown".to_string() |
| 35 | } else { |
| 36 | options.session.model.clone() |
| 37 | }; |
| 38 | |
| 39 | let tool = AITool::Cli(options.session.agent_name.clone()); |
| 40 | |
| 41 | let prompt_content = match &options.prompt { |
| 42 | Some(prompt) if !prompt.is_empty() => PromptContent::Hashed(Hash::of(prompt.as_bytes())), |
| 43 | _ => PromptContent::None, |
| 44 | }; |
| 45 | |
| 46 | let timestamp = options.event.timestamp.timestamp(); |
| 47 | |
| 48 | // Extract enriched metadata from the raw JSON payload sent by the plugin. |
| 49 | // The plugin accumulates data across all events within a turn and sends |
| 50 | // it in the `stop` payload. All fields are optional — old plugins that |
| 51 | // don't send them will simply leave these as None/default. |
| 52 | let raw = options.event.raw_json.as_ref(); |
| 53 | |
| 54 | // Helper closures for extracting typed values from raw JSON |
| 55 | let raw_str = |key: &str| -> Option<String> { |
| 56 | raw.and_then(|r| r.get(key)) |
| 57 | .and_then(|v| v.as_str()) |
| 58 | .map(|s| s.to_string()) |
| 59 | }; |
| 60 | let raw_u64 = |
| 61 | |key: &str| -> Option<u64> { raw.and_then(|r| r.get(key)).and_then(|v| v.as_u64()) }; |
| 62 | let raw_f64 = |
| 63 | |key: &str| -> Option<f64> { raw.and_then(|r| r.get(key)).and_then(|v| v.as_f64()) }; |
| 64 | let raw_u32 = |key: &str| -> Option<u32> { |
| 65 | raw.and_then(|r| r.get(key)) |
| 66 | .and_then(|v| v.as_u64()) |
| 67 | .map(|n| n as u32) |
| 68 | }; |
| 69 | |
| 70 | // Token usage — now includes reasoning tokens |
| 71 | let input = raw_u64("input_tokens").unwrap_or(0); |
| 72 | let output = raw_u64("output_tokens").unwrap_or(0); |
| 73 | let reasoning = raw_u64("reasoning_tokens").unwrap_or(0); |
| 74 | let cache_read = raw_u64("cache_read_tokens").unwrap_or(0); |
| 75 | let cache_write = raw_u64("cache_write_tokens").unwrap_or(0); |
| 76 | |
| 77 | let tokens = if input > 0 || output > 0 || reasoning > 0 || cache_read > 0 || cache_write > 0 { |
| 78 | TokenUsage::full(input, output, reasoning, cache_read, cache_write) |
| 79 | } else { |
| 80 | TokenUsage::default() |
| 81 | }; |
| 82 | |
| 83 | // Cost |