pub fn session_stats(messages: &[AgentMessage]) -> SessionStatsExpand description
Fold a conversation into a SessionStats.
Contract:
turns/token sums iterate everyAgentMessage::Assistant { usage: Some(u), .. }; a missingcache_read_tokens/cache_write_tokenscounts as 0.- (+)
cache_turnscounts those same messages when EITHER cache field isSome— the rate’s denominator, so an unreported cache is omitted, never0%. toolstallies everyContentBlock::ToolCall { name, .. }across Assistant messages (independently of whether a matching ToolResult exists), then sorts bycallsdesc, name asc.- (+) a first pass maps
ContentBlock::ToolCall { id, name, .. }→(name, tally index); a second walk overAgentMessage::ToolResult { tool_call_id, is_error, output, .. }bumpserrors(whenis_error) and addsoutput.len()toresult_byteson THAT call’s row. The result’s ownnameis never used for attribution — the id is the pairing (message.rs:59tool_call_id: String,). - (+) a
ToolCallwith NO matchingToolResultiscalls += 1, errors += 0: a mid-run snapshot can legitimately catch a call in flight, and a lost result is not a failure. Counting it as an error would invent one. - (+) a
ToolResultwhosetool_call_idmatches noToolCallbumps nothing: there is no name to attribute it to (this is what a compacted prefix looks like). The old doc’s “does not depend on the pairing invariant” claim is thereby AMENDED, not weakened — it still holds forcalls, and no longer forerrors/result_bytes. - (+)
tool_errors= Σ errors over the tallies. - (+)
costis leftNone: pricing needs the provider’sbase_url/model, which noAgentMessagecarries. The caller that knows it (the actor’sRequest::Usagearm) setscostwithpricing::cost_of(..)on the SAME slice, soturns == priced + unpricedthere. last_input_tokensis theinput_tokensof the last Assistant withSome(usage).- (+)
is_errorincludes the loop’s synthesized aborts (loop_.rs:650/loop_.rs:798), soerrorsmeans “the call did not succeed”, not “the tool failed” — say it in the doc, and in D028. - Empty input ⇒
SessionStats::default().