Files
codex/codex-rs/ext/goal/src/steering.rs
T
jif-oai c875bc8a33 Use templates for goal steering prompts (#25576)
## Why

Goal steering prompts have grown into long inline Rust strings, which
makes the authored prompt text hard to review and easy to damage while
changing the surrounding plumbing. Moving those prompts into embedded
Markdown templates keeps the policy text in the shape reviewers actually
read, while preserving the existing runtime substitution and objective
escaping behavior.

## What changed

- Added `ext/goal/templates/goals/continuation.md`, `budget_limit.md`,
and `objective_updated.md` for the three goal steering prompts.
- Updated `ext/goal/src/steering.rs` to parse those embedded templates
once with `codex-utils-template` and render the existing goal values
into them.
- Kept user objectives XML-escaped before rendering and converted budget
counters into template variables.
- Added the template directory to `ext/goal/BUILD.bazel` `compile_data`
so Bazel has the same embedded prompt inputs as Cargo.

## Testing

- Not run locally.
2026-06-01 10:55:14 +02:00

130 lines
4.4 KiB
Rust

use codex_core::context::ContextualUserFragment;
use codex_core::context::InternalContextSource;
use codex_core::context::InternalModelContextFragment;
use codex_protocol::models::ResponseItem;
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 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: &'static 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) fn continuation_steering_item(goal: &ThreadGoal) -> ResponseItem {
goal_context_input_item(continuation_prompt(goal))
}
fn goal_context_input_item(prompt: String) -> ResponseItem {
ContextualUserFragment::into(InternalModelContextFragment::new(
InternalContextSource::from_static("goal"),
prompt,
))
}
fn continuation_prompt(goal: &ThreadGoal) -> String {
let objective = escape_xml_text(&goal.objective);
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());
CONTINUATION_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/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('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
}