Allow user-requested goal pauses through update_goal (#44290)

## Why

`update_goal` only accepted `complete` and `blocked`, preventing the agent from pausing a goal in response to an explicit user request.

## What changed

- Accept `paused` and account for final goal progress when pausing, with budget limits taking precedence.
- Update tool instructions and goal prompts to allow pauses only at the user's explicit request, report the returned status, and stop goal work. A later resume revokes the pause request.

## Testing

Extend coverage for pause accounting, budget-limit precedence, rejection of resume and system-limit statuses, and preservation of a tool-paused goal when resuming a thread.

GitOrigin-RevId: c3c15a51f848ce6eb65854d51b1a1bc75bb456a7
This commit is contained in:
Eric Traut authored and copyberry committed 2026-09-09 20:03:38 +00:00
1 parent d117c2eb02
commit fa7af3883d
7 files changed
+187 -102

No files matched your search

@@ -11,6 +11,7 @@ use app_test_support::create_fake_rollout_with_text_elements;
use app_test_support::create_fake_rollout_with_token_usage;
use app_test_support::create_final_assistant_message_sse_response;
use app_test_support::create_mock_responses_server_repeating_assistant;
use app_test_support::create_mock_responses_server_sequence;
use app_test_support::create_mock_responses_server_sequence_unchecked;
use app_test_support::rollout_path;
use app_test_support::test_absolute_path;
@@ -2894,8 +2895,25 @@ async fn thread_resume_rejects_archived_session_by_id() -> Result<()> {
}
#[tokio::test]
async fn thread_resume_keeps_paused_goal_paused() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
async fn thread_resume_keeps_tool_paused_goal_paused() -> Result<()> {
let server = create_mock_responses_server_sequence(vec![
responses::sse(vec![
responses::ev_response_created("create-goal"),
responses::ev_function_call(
"create-goal-call",
"create_goal",
r#"{"objective":"keep polishing"}"#,
),
responses::ev_completed("create-goal"),
]),
responses::sse(vec![
responses::ev_response_created("pause-goal"),
responses::ev_function_call("pause-goal-call", "update_goal", r#"{"status":"paused"}"#),
responses::ev_completed("pause-goal"),
]),
create_final_assistant_message_sse_response("The goal is paused.")?,
])
.await;
let codex_home = TempDir::new()?;
mock_responses_config(&server.uri()).write(codex_home.path())?;
let config_path = codex_home.path().join("config.toml");
@@ -2925,7 +2943,7 @@ async fn thread_resume_keeps_paused_goal_paused() -> Result<()> {
thread_id: thread.id.clone(),
client_user_message_id: None,
input: vec![UserInput::Text {
text: "materialize this thread".to_string(),
text: "Create a goal to keep polishing, then pause it.".to_string(),
text_elements: Vec::new(),
}],
..Default::default()
@@ -2943,22 +2961,14 @@ async fn thread_resume_keeps_paused_goal_paused() -> Result<()> {
.await??;
let goal_id = mcp
.send_raw_request(
"thread/goal/set",
Some(json!({
"threadId": thread.id,
"objective": "keep polishing",
"status": "paused",
})),
)
.send_raw_request("thread/goal/get", Some(json!({ "threadId": thread.id })))
.await?;
let _goal: ThreadGoalSetResponse =
let goal: ThreadGoalGetResponse =
timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(goal_id)).await??;
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("thread/goal/updated"),
)
.await??;
assert_eq!(
goal.goal.expect("goal should exist").status,
ThreadGoalStatus::Paused
);
mcp.clear_message_buffer();
let resume_id = mcp