use crate::agent::runloop::unified::turn::context::TurnProcessingContext;
use vtcode_core::llm::provider::{Message, ToolDefinition};
#[derive(Copy, Clone)]
pub(super) struct ToolCatalogCacheMetrics<'a> {
pub step_count: usize,
pub model: &'a str,
pub cache_hit: bool,
pub planning_active: bool,
pub request_user_input_enabled: bool,
pub available_tools: usize,
pub stable_prefix_hash: u64,
pub tool_catalog_hash: Option<u64>,
pub prefix_change_reason: &'a str,
}
pub(super) fn emit_tool_catalog_cache_metrics(ctx: &TurnProcessingContext<'_>, metrics: ToolCatalogCacheMetrics<'_>) {
tracing::info!(
target: "vtcode.turn.metrics",
metric = "tool_catalog_cache",
run_id = %ctx.harness_state.run_id.0,
turn_id = %ctx.harness_state.turn_id.0,
turn = metrics.step_count,
model = metrics.model,
cache_hit = metrics.cache_hit,
planning_workflow = metrics.planning_active,
request_user_input_enabled = metrics.request_user_input_enabled,
available_tools = metrics.available_tools,
stable_prefix_hash = metrics.stable_prefix_hash,
tool_catalog_hash = metrics.tool_catalog_hash,
prefix_change_reason = metrics.prefix_change_reason,
"turn metric"
);
#[derive(serde::Serialize)]
struct ToolCatalogCacheRecord<'a> {
kind: &'static str,
turn: usize,
model: &'a str,
cache_hit: bool,
planning_active: bool,
request_user_input_enabled: bool,
available_tools: usize,
stable_prefix_hash: u64,
tool_catalog_hash: Option<u64>,
prefix_change_reason: &'a str,
ts: i64,
}
ctx.traj.log(&ToolCatalogCacheRecord {
kind: "tool_catalog_cache_metrics",
turn: metrics.step_count,
model: metrics.model,
cache_hit: metrics.cache_hit,
planning_active: metrics.planning_active,
request_user_input_enabled: metrics.request_user_input_enabled,
available_tools: metrics.available_tools,
stable_prefix_hash: metrics.stable_prefix_hash,
tool_catalog_hash: metrics.tool_catalog_hash,
prefix_change_reason: metrics.prefix_change_reason,
ts: chrono::Utc::now().timestamp(),
});
}
#[expect(clippy::too_many_arguments)]
pub(super) fn emit_llm_retry_metrics(
ctx: &TurnProcessingContext<'_>,
step_count: usize,
model: &str,
planning_active: bool,
attempts_made: usize,
max_retries: usize,
success: bool,
stream_fallback_used: bool,
last_error_retryable: Option<bool>,
last_error_preview: Option<&str>,
) {
let retries_used = attempts_made.saturating_sub(1);
let exhausted_retry_budget = !success && attempts_made >= max_retries;
tracing::info!(
target: "vtcode.turn.metrics",
metric = "llm_retry_outcome",
run_id = %ctx.harness_state.run_id.0,
turn_id = %ctx.harness_state.turn_id.0,
turn = step_count,
model,
planning_active,
attempts_made,
retries_used,
max_retries,
success,
exhausted_retry_budget,
stream_fallback_used,
last_error_retryable = last_error_retryable.unwrap_or(false),
"turn metric"
);
#[derive(serde::Serialize)]
struct LlmRetryMetricsRecord<'a> {
kind: &'static str,
turn: usize,
model: &'a str,
planning_active: bool,
attempts_made: usize,
retries_used: usize,
max_retries: usize,
success: bool,
exhausted_retry_budget: bool,
stream_fallback_used: bool,
last_error_retryable: Option<bool>,
last_error: Option<&'a str>,
ts: i64,
}
ctx.traj.log(&LlmRetryMetricsRecord {
kind: "llm_retry_metrics",
turn: step_count,
model,
planning_active,
attempts_made,
retries_used,
max_retries,
success,
exhausted_retry_budget,
stream_fallback_used,
last_error_retryable,
last_error: last_error_preview,
ts: chrono::Utc::now().timestamp(),
});
}
pub(super) fn estimate_tool_schema_tokens(tools: &[ToolDefinition]) -> usize {
tools
.iter()
.map(|tool| serde_json::to_string(tool).map(|s| s.len() / 4).unwrap_or(0))
.sum()
}
pub(super) fn estimate_message_history_tokens(messages: &[Message]) -> usize {
messages.iter().map(|message| message.content.as_text().len() / 4).sum()
}
#[derive(Copy, Clone)]
pub(super) struct TokenBudgetBreakdown<'a> {
pub step_count: usize,
pub model: &'a str,
pub system_prompt_tokens: usize,
pub tool_schema_tokens: usize,
pub message_history_tokens: usize,
pub on_wire_tools: usize,
pub client_local_deferral: bool,
pub tool_free_recovery: bool,
}
pub(super) fn emit_token_budget_breakdown(ctx: &TurnProcessingContext<'_>, breakdown: TokenBudgetBreakdown<'_>) {
tracing::info!(
target: "vtcode.turn.metrics",
metric = "token_budget_breakdown",
run_id = %ctx.harness_state.run_id.0,
turn_id = %ctx.harness_state.turn_id.0,
turn = breakdown.step_count,
model = breakdown.model,
system_prompt_tokens = breakdown.system_prompt_tokens,
tool_schema_tokens = breakdown.tool_schema_tokens,
message_history_tokens = breakdown.message_history_tokens,
on_wire_tools = breakdown.on_wire_tools,
client_local_deferral = breakdown.client_local_deferral,
tool_free_recovery = breakdown.tool_free_recovery,
"turn metric"
);
#[derive(serde::Serialize)]
struct TokenBudgetBreakdownRecord<'a> {
kind: &'static str,
turn: usize,
model: &'a str,
system_prompt_tokens: usize,
tool_schema_tokens: usize,
message_history_tokens: usize,
on_wire_tools: usize,
client_local_deferral: bool,
tool_free_recovery: bool,
ts: i64,
}
ctx.traj.log(&TokenBudgetBreakdownRecord {
kind: "token_budget_breakdown",
turn: breakdown.step_count,
model: breakdown.model,
system_prompt_tokens: breakdown.system_prompt_tokens,
tool_schema_tokens: breakdown.tool_schema_tokens,
message_history_tokens: breakdown.message_history_tokens,
on_wire_tools: breakdown.on_wire_tools,
client_local_deferral: breakdown.client_local_deferral,
tool_free_recovery: breakdown.tool_free_recovery,
ts: chrono::Utc::now().timestamp(),
});
}