mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Addresses #22833, #22245, #23067 ## Why `/goal` can keep synthesizing turns even when the next turn cannot make meaningful progress. Hard usage exhaustion can replay failing turns, and repeated permission or external-resource blockers can keep burning tokens while waiting for user or system intervention. ## What changed - Add resumable `blocked` and `usageLimited` goal states. As with `paused`, goal continuation stops with these states. - Move to `usageLimited` after usage-limit failures. - Allow the built-in `update_goal` tool to set `blocked` only under explicit repeated-impasse guidance. Updated goal continuation prompt to specify that agent should use `blocked` only when it has made at least three attempts to get past an impasse. Most of the files touched by this PR are because of the small app server protocol update. ## Validation I manually reproduced a number of situations where an agent can run into a true impasse and verified that it properly enters `blocked` state. I then resumed and verified that it once again entered `blocked` state several turns later if the impasse still exists. I also manually reproduced the usage-limit condition by creating a simulated responses API endpoint that returns 429 errors with the appropriate error message. Verified that the goal runtime properly moves the goal into `usageLimited` state and TUI UI updates appropriately. Verified that `/goal resume` resumes (and immediately goes back into `ussageLImited` state if appropriate). ## Follow-up PRs Small changes will be needed to the GUI clients to properly handle the two new states.
116 lines
3.2 KiB
Rust
116 lines
3.2 KiB
Rust
use anyhow::Result;
|
|
use anyhow::anyhow;
|
|
use chrono::DateTime;
|
|
use chrono::Utc;
|
|
use codex_protocol::ThreadId;
|
|
use sqlx::Row;
|
|
use sqlx::sqlite::SqliteRow;
|
|
|
|
use super::epoch_millis_to_datetime;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum ThreadGoalStatus {
|
|
Active,
|
|
Paused,
|
|
Blocked,
|
|
UsageLimited,
|
|
BudgetLimited,
|
|
Complete,
|
|
}
|
|
|
|
impl ThreadGoalStatus {
|
|
pub fn as_str(self) -> &'static str {
|
|
match self {
|
|
Self::Active => "active",
|
|
Self::Paused => "paused",
|
|
Self::Blocked => "blocked",
|
|
Self::UsageLimited => "usage_limited",
|
|
Self::BudgetLimited => "budget_limited",
|
|
Self::Complete => "complete",
|
|
}
|
|
}
|
|
|
|
pub fn is_active(self) -> bool {
|
|
self == Self::Active
|
|
}
|
|
|
|
pub fn is_terminal(self) -> bool {
|
|
matches!(self, Self::BudgetLimited | Self::Complete)
|
|
}
|
|
}
|
|
|
|
impl TryFrom<&str> for ThreadGoalStatus {
|
|
type Error = anyhow::Error;
|
|
|
|
fn try_from(value: &str) -> Result<Self> {
|
|
match value {
|
|
"active" => Ok(Self::Active),
|
|
"paused" => Ok(Self::Paused),
|
|
"blocked" => Ok(Self::Blocked),
|
|
"usage_limited" => Ok(Self::UsageLimited),
|
|
"budget_limited" => Ok(Self::BudgetLimited),
|
|
"complete" => Ok(Self::Complete),
|
|
other => Err(anyhow!("unknown thread goal status `{other}`")),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct ThreadGoal {
|
|
pub thread_id: ThreadId,
|
|
pub goal_id: String,
|
|
pub objective: String,
|
|
pub status: ThreadGoalStatus,
|
|
pub token_budget: Option<i64>,
|
|
pub tokens_used: i64,
|
|
pub time_used_seconds: i64,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
pub(crate) struct ThreadGoalRow {
|
|
pub thread_id: String,
|
|
pub goal_id: String,
|
|
pub objective: String,
|
|
pub status: String,
|
|
pub token_budget: Option<i64>,
|
|
pub tokens_used: i64,
|
|
pub time_used_seconds: i64,
|
|
pub created_at_ms: i64,
|
|
pub updated_at_ms: i64,
|
|
}
|
|
|
|
impl ThreadGoalRow {
|
|
pub(crate) fn try_from_row(row: &SqliteRow) -> Result<Self> {
|
|
Ok(Self {
|
|
thread_id: row.try_get("thread_id")?,
|
|
goal_id: row.try_get("goal_id")?,
|
|
objective: row.try_get("objective")?,
|
|
status: row.try_get("status")?,
|
|
token_budget: row.try_get("token_budget")?,
|
|
tokens_used: row.try_get("tokens_used")?,
|
|
time_used_seconds: row.try_get("time_used_seconds")?,
|
|
created_at_ms: row.try_get("created_at_ms")?,
|
|
updated_at_ms: row.try_get("updated_at_ms")?,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl TryFrom<ThreadGoalRow> for ThreadGoal {
|
|
type Error = anyhow::Error;
|
|
|
|
fn try_from(row: ThreadGoalRow) -> Result<Self> {
|
|
Ok(Self {
|
|
thread_id: ThreadId::try_from(row.thread_id)?,
|
|
goal_id: row.goal_id,
|
|
objective: row.objective,
|
|
status: ThreadGoalStatus::try_from(row.status.as_str())?,
|
|
token_budget: row.token_budget,
|
|
tokens_used: row.tokens_used,
|
|
time_used_seconds: row.time_used_seconds,
|
|
created_at: epoch_millis_to_datetime(row.created_at_ms)?,
|
|
updated_at: epoch_millis_to_datetime(row.updated_at_ms)?,
|
|
})
|
|
}
|
|
}
|