[codex] Steer budget-limited goal extension turns (#23718)

## What
- Add a small extension capability for injecting model-visible response
items into the active turn
- Have the goal extension inject hidden goal-context steering when
tool-finish accounting reaches `BudgetLimited`
- Cover the extension backend path with an assertion on the injected
steering item

## Why
PR #23696 persists and emits the budget-limited goal update from
tool-finish accounting, but it leaves the model unaware of that
transition. The existing core runtime steers the model to wrap up in
this case; the extension path should do the same through an explicit
host capability.

## Testing
- `just fmt`
- `cargo test -p codex-goal-extension`
- `cargo test -p codex-extension-api`
This commit is contained in:
jif-oai
2026-05-21 12:54:00 +02:00
committed by GitHub
parent 20fedafff8
commit 791b69dd53
14 changed files with 298 additions and 34 deletions
@@ -31,10 +31,7 @@ fn detects_subagent_notification_fragment_case_insensitively() {
#[test]
fn detects_goal_context_fragment() {
let text = GoalContext {
prompt: "Continue working toward the active thread goal.".to_string(),
}
.render();
let text = GoalContext::new("Continue working toward the active thread goal.").render();
assert!(is_contextual_user_fragment(&ContentItem::InputText {
text
@@ -43,9 +40,9 @@ fn detects_goal_context_fragment() {
#[test]
fn contextual_user_fragment_is_dyn_compatible() {
let fragment: Box<dyn ContextualUserFragment> = Box::new(GoalContext {
prompt: "Continue working toward the active thread goal.".to_string(),
});
let fragment: Box<dyn ContextualUserFragment> = Box::new(GoalContext::new(
"Continue working toward the active thread goal.",
));
assert_eq!(
fragment.render(),
+25 -2
View File
@@ -1,10 +1,33 @@
//! Hidden user-context fragment for runtime-owned goal steering prompts.
use super::ContextualUserFragment;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseInputItem;
/// Hidden runtime-owned goal steering context injected into model input.
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct GoalContext {
pub(crate) prompt: String,
pub struct GoalContext {
prompt: String,
}
impl GoalContext {
/// Creates goal context around an already-rendered steering prompt.
pub fn new(prompt: impl Into<String>) -> Self {
Self {
prompt: prompt.into(),
}
}
/// Converts the registered fragment into an active-turn injectable item.
pub fn into_response_input_item(self) -> ResponseInputItem {
ResponseInputItem::Message {
role: <Self as ContextualUserFragment>::role().to_string(),
content: vec![ContentItem::InputText {
text: self.render(),
}],
phase: None,
}
}
}
impl ContextualUserFragment for GoalContext {
+1 -1
View File
@@ -40,7 +40,7 @@ pub(crate) use environment_context::EnvironmentContext;
pub use fragment::ContextualUserFragment;
pub(crate) use fragment::FragmentRegistration;
pub(crate) use fragment::FragmentRegistrationProxy;
pub(crate) use goal_context::GoalContext;
pub use goal_context::GoalContext;
pub(crate) use guardian_followup_review_reminder::GuardianFollowupReviewReminder;
pub(crate) use hook_additional_context::HookAdditionalContext;
pub(crate) use image_generation_instructions::ImageGenerationInstructions;