Add goal lifecycle metrics (#20799)

## Why

Adding goal metrics makes it possible to track how often goals are
created, completed, and stopped by budget limits, plus the final token
and wall-clock usage for terminal outcomes.

## What Changed

- Added OpenTelemetry metric constants for goal lifecycle tracking:
- `codex.goal.created`: increments each time a new persisted goal is
created or an existing goal is replaced with a new objective.
- `codex.goal.completed`: increments when a goal transitions to
`complete`.
- `codex.goal.budget_limited`: increments when a goal transitions to
`budget_limited` because its token budget has been reached.
- `codex.goal.token_count`: records the final persisted token count when
a goal transitions to `complete` or `budget_limited`.
- `codex.goal.duration_s`: records the final persisted elapsed
wall-clock time, in seconds, when a goal transitions to `complete` or
`budget_limited`.
- Emitted creation metrics when a goal is created or replaced.
- Emitted terminal outcome counters and final usage histograms when a
goal transitions to `complete` or `budget_limited`, avoiding
double-counting later in-flight accounting for already budget-limited
goals.
- Added focused `codex-core` tests for create/complete metrics and
one-time budget-limit metrics.
This commit is contained in:
Eric Traut
2026-05-05 09:21:54 -07:00
committed by GitHub
Unverified
parent 69283aa1c0
commit 91b7350187
7 changed files with 201 additions and 55 deletions
+15 -5
View File
@@ -52,6 +52,8 @@ use codex_protocol::request_permissions::PermissionGrantScope;
use codex_protocol::request_permissions::RequestPermissionProfile;
use tracing::Span;
use crate::goals::ExternalGoalPreviousStatus;
use crate::goals::ExternalGoalSet;
use crate::goals::GoalRuntimeEvent;
use crate::goals::SetGoalRequest;
use crate::rollout::recorder::RolloutRecorder;
@@ -7505,19 +7507,24 @@ async fn external_goal_mutation_accounts_active_turn_before_status_change() -> a
.expect("goal should remain persisted");
assert_eq!(70, goal.tokens_used);
state_db
let previous_status = goal.status;
let goal_id = goal.goal_id.clone();
let updated_goal = state_db
.update_thread_goal(
sess.conversation_id,
codex_state::ThreadGoalUpdate {
status: Some(codex_state::ThreadGoalStatus::Complete),
token_budget: None,
expected_goal_id: Some(goal.goal_id),
expected_goal_id: Some(goal_id),
},
)
.await?
.expect("goal status update should succeed");
sess.goal_runtime_apply(GoalRuntimeEvent::ExternalSet {
status: codex_state::ThreadGoalStatus::Complete,
external_set: ExternalGoalSet {
goal: updated_goal,
previous_status: ExternalGoalPreviousStatus::Existing(previous_status),
},
})
.await?;
@@ -7549,7 +7556,7 @@ async fn external_active_goal_set_marks_current_turn_for_accounting() -> anyhow:
set_total_token_usage(&sess, post_goal_token_usage()).await;
let state_db = goal_test_state_db(sess.as_ref()).await?;
state_db
let goal = state_db
.replace_thread_goal(
sess.conversation_id,
"Keep improving the benchmark",
@@ -7558,7 +7565,10 @@ async fn external_active_goal_set_marks_current_turn_for_accounting() -> anyhow:
)
.await?;
sess.goal_runtime_apply(GoalRuntimeEvent::ExternalSet {
status: codex_state::ThreadGoalStatus::Active,
external_set: ExternalGoalSet {
goal,
previous_status: ExternalGoalPreviousStatus::NewGoal,
},
})
.await?;