Expose assistant content token counts in completion notifications (#59041)

## What changed

Add optional `contentTokenCount` metadata to `item/completed` notifications, carrying the producer-supplied token count for each completed assistant message in default and plan modes. Counts include any sampled footer and are `null` when unavailable.

Keep this telemetry separate from stored thread items and omit it from subsequent model requests. Update the protocol schemas and generated TypeScript and Python types.

## Testing

Add coverage for per-message counts, including zero, in default and plan modes; missing and malformed metadata; backward-compatible notification decoding; and exclusion from subsequent model requests.

GitOrigin-RevId: 35973037a42650fa2d7569da8e88cb81215cf971
This commit is contained in:
aojiao-oai
2026-11-03 02:26:29 +00:00
committed by copyberry
parent e839b828fb
commit 16ddd538bd
48 changed files with 443 additions and 39 deletions
+15
View File
@@ -2908,6 +2908,20 @@ impl Session {
&self,
turn_context: &TurnContext,
item: TurnItem,
) {
self.emit_turn_item_completed_with_token_count(
turn_context,
item,
/*content_token_count*/ None,
)
.await;
}
pub(crate) async fn emit_turn_item_completed_with_token_count(
&self,
turn_context: &TurnContext,
item: TurnItem,
content_token_count: Option<u64>,
) {
record_turn_ttfm_metric(turn_context, &item).await;
for contributor in self.services.extensions.turn_lifecycle_contributors() {
@@ -2940,6 +2954,7 @@ impl Session {
thread_id: self.thread_id,
turn_id: turn_context.sub_id.clone(),
item,
content_token_count,
started_at_ms: Some(started_at_ms),
completed_at_ms,
}),
+17 -3
View File
@@ -2565,6 +2565,7 @@ async fn emit_agent_message_in_plan_mode(
sess: &Session,
turn_context: &TurnContext,
agent_message: codex_protocol::items::AgentMessageItem,
content_token_count: Option<u64>,
state: &mut PlanModeStreamState,
) {
let agent_message_id = agent_message.id.clone();
@@ -2600,8 +2601,12 @@ async fn emit_agent_message_in_plan_mode(
.insert(agent_message_id.clone());
}
sess.emit_turn_item_completed(turn_context, TurnItem::AgentMessage(agent_message))
.await;
sess.emit_turn_item_completed_with_token_count(
turn_context,
TurnItem::AgentMessage(agent_message),
content_token_count,
)
.await;
state.started_agent_message_items.remove(&agent_message_id);
}
@@ -2610,12 +2615,20 @@ async fn emit_turn_item_in_plan_mode(
sess: &Session,
turn_context: &TurnContext,
turn_item: TurnItem,
content_token_count: Option<u64>,
previously_active_item: Option<&TurnItem>,
state: &mut PlanModeStreamState,
) {
match turn_item {
TurnItem::AgentMessage(agent_message) => {
emit_agent_message_in_plan_mode(sess, turn_context, agent_message, state).await;
emit_agent_message_in_plan_mode(
sess,
turn_context,
agent_message,
content_token_count,
state,
)
.await;
}
_ => {
if previously_active_item.is_none() {
@@ -2656,6 +2669,7 @@ async fn handle_assistant_item_done_in_plan_mode(
sess,
turn_context,
finalized_turn_item.turn_item,
finalized_turn_item.content_token_count,
previously_active_item,
state,
)