Record additional context counts, sizes, and truncation metrics (#58144)

## What changed

Add `codex.additional_context` to count submitted entries, including unchanged entries, and `codex.additional_context.value_bytes` to measure UTF-8 value sizes before and after the existing truncation limit. Sizes exclude message wrappers and include the truncation marker after truncation.

Tag metrics with context kind, truncation status, and an explicitly supplied name when present; never use opaque source keys as tags. Record metrics through the current turn's telemetry.

## Testing

Add integration coverage for message and tool-output formats, repeated submissions, truncation boundaries, empty and multibyte values, optional name tags, and turn-specific model attribution.

GitOrigin-RevId: 463f257af27be58c372970fac853b85fde495dd5
This commit is contained in:
Alex Kira
2026-10-29 18:24:34 +00:00
committed by copyberry
parent f32b6cfd2b
commit 2ca71f1683
6 changed files with 281 additions and 5 deletions
+11 -4
View File
@@ -441,7 +441,8 @@ async fn start_or_steer(
session
.maybe_emit_model_warnings_for_turn(turn_context.as_ref())
.await;
let mut task_input = merge_additional_context_input(session, additional_context).await;
let mut task_input =
merge_additional_context_input(session, &turn_context, additional_context).await;
if has_explicit_input {
task_input.push(
pending_turn_input(
@@ -591,7 +592,8 @@ async fn start_if_idle(
.maybe_emit_model_warnings_for_turn(turn_context.as_ref())
.await;
let mut task_input = merge_additional_context_input(session, additional_context).await;
let mut task_input =
merge_additional_context_input(session, &turn_context, additional_context).await;
let sender_messages = capture_sender_user_messages(session, &mut input).await;
match kind {
TurnStartKind::User => {
@@ -910,7 +912,9 @@ impl Session {
{
return Err(NotSubmittedReason::ActiveTurnOutputSchemaMismatch);
}
let mut pending_input = merge_additional_context_input(self, additional_context).await;
let mut pending_input =
merge_additional_context_input(self, &active_task.turn_context, additional_context)
.await;
active_task
.turn_context
@@ -953,6 +957,7 @@ impl Session {
async fn merge_additional_context_input(
session: &Session,
turn_context: &TurnContext,
additional_context: BTreeMap<String, AdditionalContextEntry>,
) -> Vec<TurnInput> {
let format = if session.enabled(Feature::UntrustedAsToolOutput) {
@@ -962,7 +967,9 @@ async fn merge_additional_context_input(
};
let additional_context_input = {
let mut state = session.state.lock().await;
state.additional_context.merge(additional_context, format)
state
.additional_context
.merge(additional_context, format, &turn_context.session_telemetry)
};
additional_context_input
.into_iter()
@@ -6,9 +6,15 @@ use crate::context::AdditionalContextDeveloperFragment;
use crate::context::AdditionalContextToolOutput;
use crate::context::AdditionalContextUserFragment;
use crate::context::ContextualUserFragment;
use codex_context_fragments::MAX_ADDITIONAL_CONTEXT_VALUE_TOKENS;
use codex_otel::ADDITIONAL_CONTEXT_COUNT_METRIC;
use codex_otel::ADDITIONAL_CONTEXT_VALUE_BYTES_BUCKETS;
use codex_otel::ADDITIONAL_CONTEXT_VALUE_BYTES_METRIC;
use codex_otel::SessionTelemetry;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::AdditionalContextEntry;
use codex_protocol::protocol::AdditionalContextKind;
use codex_utils_string::truncate_middle_with_token_budget;
pub(crate) enum AdditionalContextFormat {
Messages,
@@ -45,7 +51,11 @@ impl AdditionalContextStore {
&mut self,
values: BTreeMap<String, AdditionalContextEntry>,
format: AdditionalContextFormat,
telemetry: &SessionTelemetry,
) -> Vec<ResponseItem> {
for entry in values.values() {
Self::record_metrics(entry, telemetry);
}
let values =
values
.into_iter()
@@ -87,4 +97,32 @@ impl AdditionalContextStore {
self.values = values;
fragments
}
fn record_metrics(entry: &AdditionalContextEntry, telemetry: &SessionTelemetry) {
let (bounded_value, original_tokens) =
truncate_middle_with_token_budget(&entry.value, MAX_ADDITIONAL_CONTEXT_VALUE_TOKENS);
let kind = match entry.kind {
AdditionalContextKind::Untrusted => "untrusted",
AdditionalContextKind::Application => "application",
};
let truncated = original_tokens.is_some().to_string();
let mut tags = vec![("kind", kind), ("truncated", truncated.as_str())];
if let Some(name) = entry.name.as_deref() {
tags.push(("name", name));
}
telemetry.counter(ADDITIONAL_CONTEXT_COUNT_METRIC, /*inc*/ 1, &tags);
for (stage, bytes) in [
("before", entry.value.len()),
("after", bounded_value.len()),
] {
let mut size_tags = tags.clone();
size_tags.push(("stage", stage));
telemetry.histogram_with_boundaries(
ADDITIONAL_CONTEXT_VALUE_BYTES_METRIC,
i64::try_from(bytes).unwrap_or(i64::MAX),
ADDITIONAL_CONTEXT_VALUE_BYTES_BUCKETS,
&size_tags,
);
}
}
}