mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Fix goal update and add /goal edit command in TUI (#21954)
## Why Users have requested the ability to edit a goal's objective after a goal has been created. This PR exposes a new `/goal edit` command in the TUI to address this request. In the process of implementing this, I also noticed an existing bug in the goal runtime. When a goal's objective is updated through the `thread/goal/set` app server API, the goal runtime didn't emit a new steering prompt to tell the agent about the new objective. This PR also fixes this hole. ## What Changed - Adds `/goal edit` in the TUI, opening an edit box prefilled with the current goal objective. - Keeps active and paused goals in their current state, resets completed goals to active, keeps budget-limited goals budget-limited, and preserves the existing token budget. - Changes the existing `thread/goal/set` behavior so editing an objective preserves goal accounting instead of resetting it. The older reset-on-new-objective behavior was left over from before `thread/goal/clear`; clients that need to reset accounting can now clear the existing goal and create a new one. - Reuses the existing goal set API path; this does not add or change app-server protocol surface area. - Adds a dedicated goal runtime steering prompt when an externally persisted goal mutation changes the objective, so active turns receive the updated objective. ## Validation - Make sure `/goal edit` returns an error if no goal currently exists - Make sure `/goal edit` displays an edit box that can be optionally canceled with no side effects - Make sure that an edited goal results in a steer so the agent starts pursuing the new objective - Make sure the new objective is reflected in the goal if you use `/goal` to display the goal summary - Make sure that `/goal edit` doesn't reset the token budget, time/token accounting on the updated goal
This commit is contained in:
@@ -8074,12 +8074,13 @@ async fn external_goal_mutation_accounts_active_turn_before_status_change() -> a
|
||||
.expect("goal should remain persisted");
|
||||
assert_eq!(70, goal.tokens_used);
|
||||
|
||||
let previous_status = goal.status;
|
||||
let previous_goal = goal.clone();
|
||||
let goal_id = goal.goal_id.clone();
|
||||
let updated_goal = state_db
|
||||
.update_thread_goal(
|
||||
sess.conversation_id,
|
||||
codex_state::ThreadGoalUpdate {
|
||||
objective: None,
|
||||
status: Some(codex_state::ThreadGoalStatus::Complete),
|
||||
token_budget: None,
|
||||
expected_goal_id: Some(goal_id),
|
||||
@@ -8090,7 +8091,7 @@ async fn external_goal_mutation_accounts_active_turn_before_status_change() -> a
|
||||
sess.goal_runtime_apply(GoalRuntimeEvent::ExternalSet {
|
||||
external_set: ExternalGoalSet {
|
||||
goal: updated_goal,
|
||||
previous_status: ExternalGoalPreviousStatus::Existing(previous_status),
|
||||
previous_status: ExternalGoalPreviousStatus::from(&previous_goal),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
@@ -8108,6 +8109,70 @@ async fn external_goal_mutation_accounts_active_turn_before_status_change() -> a
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn external_objective_change_steers_active_turn() -> anyhow::Result<()> {
|
||||
let (sess, tc, _rx, _codex_home) = make_goal_session_and_context_with_rx().await;
|
||||
sess.spawn_task(
|
||||
Arc::clone(&tc),
|
||||
Vec::new(),
|
||||
NeverEndingTask {
|
||||
kind: TaskKind::Regular,
|
||||
listen_to_cancellation_token: false,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let state_db = goal_test_state_db(sess.as_ref()).await?;
|
||||
let old_goal = state_db
|
||||
.replace_thread_goal(
|
||||
sess.conversation_id,
|
||||
"Keep improving the benchmark",
|
||||
codex_state::ThreadGoalStatus::Active,
|
||||
/*token_budget*/ Some(10_000),
|
||||
)
|
||||
.await?;
|
||||
let new_goal = state_db
|
||||
.replace_thread_goal(
|
||||
sess.conversation_id,
|
||||
"Write a concise benchmark summary",
|
||||
codex_state::ThreadGoalStatus::Active,
|
||||
/*token_budget*/ Some(10_000),
|
||||
)
|
||||
.await?;
|
||||
|
||||
sess.goal_runtime_apply(GoalRuntimeEvent::ExternalSet {
|
||||
external_set: ExternalGoalSet {
|
||||
goal: new_goal,
|
||||
previous_status: ExternalGoalPreviousStatus::from(&old_goal),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
|
||||
let pending_input = sess.get_pending_input().await;
|
||||
assert!(
|
||||
pending_input.iter().any(|item| {
|
||||
matches!(
|
||||
item,
|
||||
ResponseInputItem::Message { role, content, .. }
|
||||
if role == "user"
|
||||
&& content.iter().any(|content| matches!(
|
||||
content,
|
||||
ContentItem::InputText { text }
|
||||
if text.starts_with("<goal_context>")
|
||||
&& text.trim_end().ends_with("</goal_context>")
|
||||
&& text.contains("The active thread goal objective was edited")
|
||||
&& text.contains("Write a concise benchmark summary")
|
||||
))
|
||||
)
|
||||
}),
|
||||
"expected objective-updated steering prompt in pending input: {pending_input:?}"
|
||||
);
|
||||
|
||||
sess.abort_all_tasks(TurnAbortReason::Replaced).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn external_active_goal_set_marks_current_turn_for_accounting() -> anyhow::Result<()> {
|
||||
let (sess, tc, _rx, _codex_home) = make_goal_session_and_context_with_rx().await;
|
||||
|
||||
Reference in New Issue
Block a user