mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[1 of 2] Align goal extension with core behavior (#26547)
## Stack 1. [#26547](https://github.com/openai/codex/pull/26547) - [1 of 2] Align goal extension with core behavior 2. [#26548](https://github.com/openai/codex/pull/26548) - [2 of 2] Move goal runtime to extension ## Why The goal runtime is moving out of `codex-core` and into `codex-goal-extension`. This first PR brings the extension back in line with the current core behavior before the follow-up PR switches app-server sessions over to the extension, so that review can focus on ownership and wiring rather than hidden behavior drift. ## What Changed - Updates the extension `create_goal` and `update_goal` tool schemas/descriptions to match the current core wording for explicit token budgets, blocked-goal audits, resumed blocked goals, and system-owned budget/usage-limit transitions. - Marks `codex-goal-extension` as the live `/goal` extension crate rather than an unwired sketch. - Looks up the live thread before reading goal state for idle continuation, so continuation setup exits early when no live thread can accept the automatic turn.
This commit is contained in:
committed by
GitHub
Unverified
parent
78eba34b41
commit
a8c9530911
@@ -124,7 +124,19 @@ impl GoalService {
|
||||
.map_err(GoalServiceError::InvalidRequest)?;
|
||||
}
|
||||
|
||||
if let Some(runtime) = self.runtime_for_thread(thread_id)
|
||||
let runtime = self.runtime_for_thread(thread_id);
|
||||
// Hold this through the prepare/write window so idle continuation cannot
|
||||
// launch from goal state that this external mutation is about to change.
|
||||
let _goal_state_permit = match runtime.as_ref() {
|
||||
Some(runtime) => Some(
|
||||
runtime
|
||||
.goal_state_permit()
|
||||
.await
|
||||
.map_err(GoalServiceError::Internal)?,
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
if let Some(runtime) = runtime.as_ref()
|
||||
&& let Err(err) = runtime.prepare_external_goal_mutation().await
|
||||
{
|
||||
tracing::warn!("failed to prepare external goal mutation: {err}");
|
||||
@@ -229,7 +241,19 @@ impl GoalService {
|
||||
state_db: &codex_state::StateRuntime,
|
||||
thread_id: ThreadId,
|
||||
) -> Result<bool, GoalServiceError> {
|
||||
if let Some(runtime) = self.runtime_for_thread(thread_id)
|
||||
let runtime = self.runtime_for_thread(thread_id);
|
||||
// Hold this through the prepare/write window so idle continuation cannot
|
||||
// launch from goal state that this external mutation is about to change.
|
||||
let goal_state_permit = match runtime.as_ref() {
|
||||
Some(runtime) => Some(
|
||||
runtime
|
||||
.goal_state_permit()
|
||||
.await
|
||||
.map_err(GoalServiceError::Internal)?,
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
if let Some(runtime) = runtime.as_ref()
|
||||
&& let Err(err) = runtime.prepare_external_goal_mutation().await
|
||||
{
|
||||
tracing::warn!("failed to prepare external goal mutation: {err}");
|
||||
@@ -242,6 +266,8 @@ impl GoalService {
|
||||
.map_err(|err| {
|
||||
GoalServiceError::Internal(format!("failed to clear thread goal: {err}"))
|
||||
})?;
|
||||
drop(goal_state_permit);
|
||||
drop(runtime);
|
||||
|
||||
if cleared
|
||||
&& let Some(runtime) = self.runtime_for_thread(thread_id)
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
//! Extension crate sketch for the `/goal` feature.
|
||||
//!
|
||||
//! This crate is intentionally not wired into the host yet. It contains the
|
||||
//! goal tool specs, extension registration shape, and the parts of runtime
|
||||
//! accounting that can be represented with today's extension API.
|
||||
//! Extension crate for the `/goal` feature.
|
||||
|
||||
mod accounting;
|
||||
mod api;
|
||||
|
||||
@@ -15,6 +15,8 @@ use crate::metrics::GoalMetrics;
|
||||
use crate::steering::continuation_steering_item;
|
||||
use crate::steering::objective_updated_steering_item;
|
||||
use crate::tool::protocol_goal_from_state;
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::sync::SemaphorePermit;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GoalRuntimeHandle {
|
||||
@@ -35,6 +37,7 @@ struct GoalRuntimeInner {
|
||||
accounting_state: Arc<GoalAccountingState>,
|
||||
enabled: AtomicBool,
|
||||
tools_available_for_thread: bool,
|
||||
goal_state_lock: Semaphore,
|
||||
}
|
||||
|
||||
pub(crate) struct AccountedGoalProgress {
|
||||
@@ -85,6 +88,7 @@ impl GoalRuntimeHandle {
|
||||
accounting_state,
|
||||
enabled: AtomicBool::new(config.enabled),
|
||||
tools_available_for_thread: config.tools_available_for_thread,
|
||||
goal_state_lock: Semaphore::new(/*permits*/ 1),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -109,6 +113,14 @@ impl GoalRuntimeHandle {
|
||||
Arc::clone(&self.inner.accounting_state)
|
||||
}
|
||||
|
||||
pub(crate) async fn goal_state_permit(&self) -> Result<SemaphorePermit<'_>, String> {
|
||||
self.inner
|
||||
.goal_state_lock
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
pub async fn prepare_external_goal_mutation(&self) -> Result<(), String> {
|
||||
if !self.is_enabled() {
|
||||
return Ok(());
|
||||
@@ -280,6 +292,18 @@ impl GoalRuntimeHandle {
|
||||
self.inner.accounting_state.clear_active_goal();
|
||||
return Ok(());
|
||||
}
|
||||
// Hold this through the read/start window so external set/clear cannot
|
||||
// change the goal after we read it but before the continuation launches.
|
||||
let _goal_state_permit = self.goal_state_permit().await?;
|
||||
|
||||
let Some(thread_manager) = self.inner.thread_manager.upgrade() else {
|
||||
tracing::debug!("skipping goal continuation because thread manager is unavailable");
|
||||
return Ok(());
|
||||
};
|
||||
let Ok(thread) = thread_manager.get_thread(self.inner.thread_id).await else {
|
||||
tracing::debug!("skipping goal continuation because live thread is unavailable");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(goal) = self
|
||||
.inner
|
||||
@@ -296,16 +320,7 @@ impl GoalRuntimeHandle {
|
||||
self.inner.accounting_state.clear_active_goal();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let item = continuation_steering_item(&protocol_goal_from_state(goal));
|
||||
let Some(thread_manager) = self.inner.thread_manager.upgrade() else {
|
||||
tracing::debug!("skipping goal continuation because thread manager is unavailable");
|
||||
return Ok(());
|
||||
};
|
||||
let Ok(thread) = thread_manager.get_thread(self.inner.thread_id).await else {
|
||||
tracing::debug!("skipping goal continuation because live thread is unavailable");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if let Err(err) = thread.try_start_turn_if_idle(vec![item]).await {
|
||||
let reason = err.reason();
|
||||
|
||||
@@ -34,7 +34,8 @@ pub fn create_create_goal_tool() -> ToolSpec {
|
||||
(
|
||||
"token_budget".to_string(),
|
||||
JsonSchema::integer(Some(
|
||||
"Optional positive token budget for the new active goal.".to_string(),
|
||||
"Positive token budget for the new goal. Omit unless explicitly requested."
|
||||
.to_string(),
|
||||
)),
|
||||
),
|
||||
]);
|
||||
@@ -62,7 +63,7 @@ pub fn create_update_goal_tool() -> ToolSpec {
|
||||
JsonSchema::string_enum(
|
||||
vec![json!("complete"), json!("blocked")],
|
||||
Some(
|
||||
"Required. Set to complete only when the objective is achieved and no required work remains. Set to blocked only when the goal cannot currently proceed without a user decision, missing dependency, or external unblock."
|
||||
"Required. Set to `complete` only when the objective is achieved and no required work remains. Set to `blocked` only after the same blocking condition has recurred for at least three consecutive goal turns and the agent is at an impasse. After a previously blocked goal is resumed, the resumed run starts a fresh blocked audit."
|
||||
.to_string(),
|
||||
),
|
||||
),
|
||||
@@ -71,11 +72,14 @@ pub fn create_update_goal_tool() -> ToolSpec {
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: UPDATE_GOAL_TOOL_NAME.to_string(),
|
||||
description: r#"Update the existing goal.
|
||||
Use this tool only to mark the goal achieved or blocked.
|
||||
Use this tool only to mark the goal achieved or genuinely blocked.
|
||||
Set status to `complete` only when the objective has actually been achieved and no required work remains.
|
||||
Set status to `blocked` only when the goal cannot currently proceed until something external changes.
|
||||
Set status to `blocked` only when the same blocking condition has repeated for at least three consecutive goal turns, counting the original/user-triggered turn and any automatic continuations, and the agent cannot make meaningful progress without user input or an external-state change.
|
||||
If the user resumes a goal that was previously marked `blocked`, treat the resumed run as a fresh blocked audit. If the same blocking condition then repeats for at least three consecutive resumed goal turns, set status to `blocked` again.
|
||||
Once the blocked threshold is satisfied, do not keep reporting that you are still blocked while leaving the goal active; set status to `blocked`.
|
||||
Do not use `blocked` merely because the work is hard, slow, uncertain, incomplete, or would benefit from clarification.
|
||||
Do not mark a goal complete merely because its budget is nearly exhausted or because you are stopping work.
|
||||
You cannot use this tool to pause, resume, or budget-limit a goal; those status changes are controlled by the user or system.
|
||||
You cannot use this tool to pause, resume, budget-limit, or usage-limit a goal; those status changes are controlled by the user or system.
|
||||
When marking a budgeted goal achieved with status `complete`, report the final token usage from the tool result to the user."#
|
||||
.to_string(),
|
||||
strict: false,
|
||||
|
||||
Reference in New Issue
Block a user