mirror of
https://github.com/openai/codex.git
synced 2026-11-13 21:30:47 +00:00
## What changed - Pass goal objectives as untrusted `goal_objective` context instead of embedding them in continuation instructions. Direct the model to use `get_goal` if the objective is missing or truncated. - Inject newly created objectives into their originating active turn without dropping unrelated client context. Render them as tool outputs when `UntrustedAsToolOutput` is enabled. - Queue additional context together with automatic turn input so both reach the first model request. ## Testing Extend app-server coverage to verify that model-written objectives appear as tool outputs at creation and continuation, without duplicating them in user messages. Update steering and Python SDK tests to check objective preservation and delivery alongside continuation instructions. GitOrigin-RevId: 6f44f6d6c895bb4dc378c9cf84d6470ccc885c67
157 lines
5.3 KiB
Rust
157 lines
5.3 KiB
Rust
use codex_core::TurnInput;
|
|
use codex_core::TurnInputRequest;
|
|
use codex_core::context::ContextualUserFragment;
|
|
use codex_core::context::InternalContextSource;
|
|
use codex_core::context::InternalModelContextFragment;
|
|
use codex_prompts::without_update_plan_instructions;
|
|
use codex_protocol::models::ResponseItem;
|
|
use codex_protocol::protocol::AdditionalContextEntry;
|
|
use codex_protocol::protocol::AdditionalContextKind;
|
|
use codex_protocol::protocol::ThreadGoal;
|
|
use codex_utils_template::Template;
|
|
use std::sync::LazyLock;
|
|
|
|
static CONTINUATION_PROMPT_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
|
|
parse_embedded_template(
|
|
include_str!("../templates/goals/continuation.md"),
|
|
"goals/continuation.md",
|
|
)
|
|
});
|
|
|
|
static CONTINUATION_PROMPT_WITHOUT_UPDATE_PLAN: LazyLock<Template> = LazyLock::new(|| {
|
|
parse_embedded_template(
|
|
&without_update_plan_instructions(include_str!("../templates/goals/continuation.md")),
|
|
"goals/continuation.md",
|
|
)
|
|
});
|
|
|
|
static BUDGET_LIMIT_PROMPT_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
|
|
parse_embedded_template(
|
|
include_str!("../templates/goals/budget_limit.md"),
|
|
"goals/budget_limit.md",
|
|
)
|
|
});
|
|
|
|
static OBJECTIVE_UPDATED_PROMPT_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
|
|
parse_embedded_template(
|
|
include_str!("../templates/goals/objective_updated.md"),
|
|
"goals/objective_updated.md",
|
|
)
|
|
});
|
|
|
|
fn parse_embedded_template(source: &str, template_name: &str) -> Template {
|
|
match Template::parse(source) {
|
|
Ok(template) => template,
|
|
Err(err) => panic!("embedded template {template_name} is invalid: {err}"),
|
|
}
|
|
}
|
|
|
|
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) -> ResponseItem {
|
|
goal_context_input_item(objective_updated_prompt(goal))
|
|
}
|
|
|
|
pub(crate) const GOAL_OBJECTIVE_CONTEXT: &str = "goal_objective";
|
|
|
|
pub(crate) fn continuation_request(
|
|
goal: &ThreadGoal,
|
|
update_plan_enabled: bool,
|
|
) -> TurnInputRequest {
|
|
let steering = goal_context_input_item(continuation_prompt(goal, update_plan_enabled));
|
|
TurnInputRequest::new(TurnInput::ResponseItem(steering)).with_additional_context(
|
|
[(
|
|
GOAL_OBJECTIVE_CONTEXT.to_owned(),
|
|
AdditionalContextEntry::new(goal.objective.clone(), AdditionalContextKind::Untrusted),
|
|
)]
|
|
.into(),
|
|
)
|
|
}
|
|
|
|
fn goal_context_input_item(prompt: String) -> ResponseItem {
|
|
ContextualUserFragment::into(InternalModelContextFragment::new(
|
|
InternalContextSource::from_static("goal"),
|
|
prompt,
|
|
))
|
|
}
|
|
|
|
fn continuation_prompt(goal: &ThreadGoal, update_plan_enabled: bool) -> String {
|
|
let tokens_used = goal.tokens_used.to_string();
|
|
let token_budget = goal
|
|
.token_budget
|
|
.map(|budget| budget.to_string())
|
|
.unwrap_or_else(|| "none".to_string());
|
|
let remaining_tokens = goal
|
|
.token_budget
|
|
.map(|budget| (budget - goal.tokens_used).max(0).to_string())
|
|
.unwrap_or_else(|| "unbounded".to_string());
|
|
|
|
let template = if update_plan_enabled {
|
|
&*CONTINUATION_PROMPT_TEMPLATE
|
|
} else {
|
|
&*CONTINUATION_PROMPT_WITHOUT_UPDATE_PLAN
|
|
};
|
|
template
|
|
.render([
|
|
("tokens_used", tokens_used.as_str()),
|
|
("token_budget", token_budget.as_str()),
|
|
("remaining_tokens", remaining_tokens.as_str()),
|
|
])
|
|
.unwrap_or_else(|err| {
|
|
panic!("embedded goals/continuation.md template failed to render: {err}")
|
|
})
|
|
}
|
|
|
|
fn budget_limit_prompt(goal: &ThreadGoal) -> String {
|
|
let objective = escape_xml_text(&goal.objective);
|
|
let time_used_seconds = goal.time_used_seconds.to_string();
|
|
let tokens_used = goal.tokens_used.to_string();
|
|
let token_budget = goal
|
|
.token_budget
|
|
.map(|budget| budget.to_string())
|
|
.unwrap_or_else(|| "none".to_string());
|
|
|
|
BUDGET_LIMIT_PROMPT_TEMPLATE
|
|
.render([
|
|
("objective", objective.as_str()),
|
|
("time_used_seconds", time_used_seconds.as_str()),
|
|
("tokens_used", tokens_used.as_str()),
|
|
("token_budget", token_budget.as_str()),
|
|
])
|
|
.unwrap_or_else(|err| {
|
|
panic!("embedded goals/budget_limit.md template failed to render: {err}")
|
|
})
|
|
}
|
|
|
|
fn objective_updated_prompt(goal: &ThreadGoal) -> String {
|
|
let objective = escape_xml_text(&goal.objective);
|
|
let tokens_used = goal.tokens_used.to_string();
|
|
let (token_budget, remaining_tokens) = match goal.token_budget {
|
|
Some(token_budget) => (
|
|
token_budget.to_string(),
|
|
(token_budget - goal.tokens_used).max(0).to_string(),
|
|
),
|
|
None => ("none".to_string(), "unknown".to_string()),
|
|
};
|
|
|
|
OBJECTIVE_UPDATED_PROMPT_TEMPLATE
|
|
.render([
|
|
("objective", objective.as_str()),
|
|
("tokens_used", tokens_used.as_str()),
|
|
("token_budget", token_budget.as_str()),
|
|
("remaining_tokens", remaining_tokens.as_str()),
|
|
])
|
|
.unwrap_or_else(|err| {
|
|
panic!("embedded goals/objective_updated.md template failed to render: {err}")
|
|
})
|
|
}
|
|
|
|
fn escape_xml_text(input: &str) -> String {
|
|
input
|
|
.replace('&', "&")
|
|
.replace('<', "<")
|
|
.replace('>', ">")
|
|
}
|