mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat: account active goal progress in the goal extension (#23696)
## Why The goal extension can create and surface goals, but the live turn-accounting path still stopped short of persisting active-goal progress. That leaves token and wall-clock usage, plus `ThreadGoalUpdated` events, out of sync with the extension boundary once work actually advances or a goal transitions out of active state. ## What changed - Teach `GoalAccountingState` to track the current turn, active goal, token deltas, and wall-clock progress snapshots against the persisted goal id. - Flush active-goal accounting from tool-finish, turn-stop, and turn-abort lifecycle hooks, and emit `ThreadGoalUpdated` events when persisted progress changes. - Route `create_goal` and `update_goal` through the same accounting state so new goals start from the right baseline, final progress is flushed before status changes, and `update_goal` can mark a goal `blocked` as well as `complete`. - Keep budget-limited goals accruing through the end of the turn while clearing local active-goal state once a turn or explicit update is finished. - Expand backend and lifecycle coverage around store ids, baseline reset, tool-finish accounting, budget-limited carry-through, and blocked-goal updates. ## Testing - Added focused backend coverage in `codex-rs/ext/goal/tests/goal_extension_backend.rs` for baseline reset, tool-finish accounting, budget-limited turns, and blocked-goal updates. - Extended `codex-rs/core/src/session/tests.rs` to assert that lifecycle inputs expose the expected session, thread, and turn store ids.
This commit is contained in:
@@ -1918,6 +1918,9 @@ async fn turn_start_lifecycle_exposes_turn_metadata_and_token_baseline() {
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct RecordedTurnStart {
|
||||
session_level_id: String,
|
||||
thread_level_id: String,
|
||||
turn_level_id: String,
|
||||
turn_id: String,
|
||||
collaboration_mode: CollaborationMode,
|
||||
token_usage_at_turn_start: TokenUsage,
|
||||
@@ -1936,6 +1939,9 @@ async fn turn_start_lifecycle_exposes_turn_metadata_and_token_baseline() {
|
||||
.lock()
|
||||
.expect("turn start records lock")
|
||||
.push(RecordedTurnStart {
|
||||
session_level_id: input.session_store.level_id().to_string(),
|
||||
thread_level_id: input.thread_store.level_id().to_string(),
|
||||
turn_level_id: input.turn_store.level_id().to_string(),
|
||||
turn_id: input.turn_id.to_string(),
|
||||
collaboration_mode: input.collaboration_mode.clone(),
|
||||
token_usage_at_turn_start: input.token_usage_at_turn_start.clone(),
|
||||
@@ -1965,52 +1971,43 @@ async fn turn_start_lifecycle_exposes_turn_metadata_and_token_baseline() {
|
||||
.insert(ThreadTurnStartMarker);
|
||||
|
||||
let token_usage_at_turn_start = TokenUsage {
|
||||
input_tokens: 120,
|
||||
cached_input_tokens: 15,
|
||||
output_tokens: 40,
|
||||
reasoning_output_tokens: 9,
|
||||
total_tokens: 169,
|
||||
input_tokens: 100,
|
||||
cached_input_tokens: 40,
|
||||
output_tokens: 25,
|
||||
reasoning_output_tokens: 5,
|
||||
total_tokens: 130,
|
||||
};
|
||||
session
|
||||
.state
|
||||
.lock()
|
||||
.await
|
||||
.set_token_info(Some(TokenUsageInfo {
|
||||
total_token_usage: token_usage_at_turn_start.clone(),
|
||||
last_token_usage: TokenUsage::default(),
|
||||
model_context_window: turn_context.model_context_window(),
|
||||
}));
|
||||
set_total_token_usage(&session, token_usage_at_turn_start.clone()).await;
|
||||
|
||||
let turn_context = Arc::new(turn_context);
|
||||
let session = Arc::new(session);
|
||||
session
|
||||
.spawn_task(
|
||||
Arc::clone(&turn_context),
|
||||
Vec::new(),
|
||||
NeverEndingTask {
|
||||
kind: TaskKind::Regular,
|
||||
listen_to_cancellation_token: true,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
let expected = RecordedTurnStart {
|
||||
session_level_id: session.session_id().to_string(),
|
||||
thread_level_id: session.conversation_id.to_string(),
|
||||
turn_level_id: turn_context.sub_id.clone(),
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
collaboration_mode: turn_context.collaboration_mode.clone(),
|
||||
token_usage_at_turn_start,
|
||||
saw_session_store: true,
|
||||
saw_thread_store: true,
|
||||
};
|
||||
|
||||
session.abort_all_tasks(TurnAbortReason::Interrupted).await;
|
||||
let sess = Arc::new(session);
|
||||
sess.spawn_task(
|
||||
Arc::new(turn_context),
|
||||
Vec::new(),
|
||||
NeverEndingTask {
|
||||
kind: TaskKind::Regular,
|
||||
listen_to_cancellation_token: true,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
sess.abort_all_tasks(TurnAbortReason::Interrupted).await;
|
||||
|
||||
let actual = records
|
||||
.lock()
|
||||
.expect("turn start records lock")
|
||||
.drain(..)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
vec![RecordedTurnStart {
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
collaboration_mode: turn_context.collaboration_mode.clone(),
|
||||
token_usage_at_turn_start,
|
||||
saw_session_store: true,
|
||||
saw_thread_store: true,
|
||||
}],
|
||||
actual
|
||||
);
|
||||
assert_eq!(vec![expected], actual);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user