Use inject_if_running for active goal steering (#24924)

## Why

This PR is stacked on #24918, which moves goal steering onto
source-labeled internal model context fragments. Active-turn goal
steering should use the same running-turn injection path as other
runtime steering, so those fragments enter the pending input queue as
`ResponseItem`s through the existing
[`Session::inject_if_running`](8d6f6cdf69/codex-rs/core/src/session/inject.rs (L12-L27))
behavior instead of through a goal-specific conversion wrapper.

## What Changed

- Exposes a narrow `CodexThread::inject_if_running` bridge for callers
that only hold a thread handle.
- Changes `ext/goal` active-turn steering to pass `ResponseItem`s
directly.
- Builds goal steering prompts as contextual internal model context
`ResponseItem`s before injecting them into the running turn.

## Testing

Not run locally; PR metadata update only.
This commit is contained in:
jif-oai
2026-05-29 11:24:39 +02:00
committed by GitHub
parent 740d942f90
commit 8f6a945ec9
3 changed files with 19 additions and 26 deletions

View File

@@ -18,7 +18,6 @@ use codex_protocol::mcp::CallToolResult;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::ContentItem;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AdditionalContextEntry;
@@ -265,20 +264,16 @@ impl CodexThread {
.await
}
/// Injects hidden model-visible items into the currently active turn.
/// Injects model-visible items into the currently active turn.
///
/// This is the runtime-owned counterpart to user-facing `steer_input`.
/// This is the thread-level bridge to `Session::inject_if_running` for
/// callers that only hold a `CodexThread`.
/// It returns the unchanged items when this thread has no active turn.
pub async fn inject_response_items_into_active_turn(
pub async fn inject_if_running(
&self,
items: Vec<ResponseInputItem>,
) -> Result<(), Vec<ResponseInputItem>> {
let response_items = items.iter().cloned().map(ResponseItem::from).collect();
self.codex
.session
.inject_if_running(response_items)
.await
.map_err(|_| items)
items: Vec<ResponseItem>,
) -> Result<(), Vec<ResponseItem>> {
self.codex.session.inject_if_running(items).await
}
pub async fn set_app_server_client_info(
@@ -396,7 +391,7 @@ impl CodexThread {
.await;
}
/// Append raw Responses API items to the thread's model-visible history.
/// Record raw Responses API items without starting a new turn.
pub async fn inject_response_items(&self, items: Vec<ResponseItem>) -> CodexResult<()> {
if items.is_empty() {
return Err(CodexErr::InvalidRequest(

View File

@@ -5,7 +5,7 @@ use std::sync::atomic::Ordering;
use codex_core::ThreadManager;
use codex_protocol::ThreadId;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::ThreadGoal;
use crate::accounting::BudgetLimitedGoalDisposition;
@@ -275,7 +275,7 @@ impl GoalRuntimeHandle {
Ok(())
}
pub(crate) async fn inject_active_turn_steering(&self, item: ResponseInputItem) {
pub(crate) async fn inject_active_turn_steering(&self, item: ResponseItem) {
let Some(thread_manager) = self.inner.thread_manager.upgrade() else {
tracing::debug!("skipping goal steering because thread manager is unavailable");
return;
@@ -284,11 +284,7 @@ impl GoalRuntimeHandle {
tracing::debug!("skipping goal steering because live thread is unavailable");
return;
};
if thread
.inject_response_items_into_active_turn(vec![item])
.await
.is_err()
{
if thread.inject_if_running(vec![item]).await.is_err() {
tracing::debug!("skipping goal steering because no turn is active");
}
}

View File

@@ -1,20 +1,22 @@
use codex_core::context::ContextualUserFragment;
use codex_core::context::InternalContextSource;
use codex_core::context::InternalModelContextFragment;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::ThreadGoal;
pub(crate) fn budget_limit_steering_item(goal: &ThreadGoal) -> ResponseInputItem {
pub(crate) fn budget_limit_steering_item(goal: &ThreadGoal) -> ResponseItem {
goal_context_input_item(budget_limit_prompt(goal))
}
pub(crate) fn objective_updated_steering_item(goal: &ThreadGoal) -> ResponseInputItem {
pub(crate) fn objective_updated_steering_item(goal: &ThreadGoal) -> ResponseItem {
goal_context_input_item(objective_updated_prompt(goal))
}
fn goal_context_input_item(prompt: String) -> ResponseInputItem {
InternalModelContextFragment::new(InternalContextSource::from_static("goal"), prompt)
.into_response_input_item()
fn goal_context_input_item(prompt: String) -> ResponseItem {
ContextualUserFragment::into(InternalModelContextFragment::new(
InternalContextSource::from_static("goal"),
prompt,
))
}
fn budget_limit_prompt(goal: &ThreadGoal) -> String {