rendering

This commit is contained in:
jif-oai
2026-05-02 12:10:17 +01:00
parent d2aa463545
commit e337e9b216
4 changed files with 187 additions and 1 deletions
+3
View File
@@ -213,6 +213,9 @@ where
#[cfg(test)]
mod tests {
use super::*;
use codex_features::CommonFeatureConfigToml;
use codex_features::FeatureConfigTable;
use codex_features::FeatureToml;
use pretty_assertions::assert_eq;
use std::sync::Arc;
+136
View File
@@ -0,0 +1,136 @@
use std::collections::BTreeMap;
use codex_features::FEATURES;
use codex_protocol::models::ResponseItem;
use codex_utils_template::Template;
use codex_utils_template::TemplateError;
use codex_utils_template::TemplateRenderError;
use toml::Value as TomlValue;
use tracing::warn;
use crate::context_manager::updates::build_developer_update_item;
use super::session::SessionConfiguration;
use super::turn_context::TurnContext;
pub(super) fn render_feature_hint_messages(
session_configuration: &SessionConfiguration,
turn_context: &TurnContext,
) -> Vec<ResponseItem> {
let lockfile = match session_configuration.to_config_lockfile_toml() {
Ok(lockfile) => lockfile,
Err(err) => {
warn!(error = %err, "failed to build config lock for feature hint rendering");
return Vec::new();
}
};
let Some(features_toml) = lockfile.config.features.as_ref() else {
return Vec::new();
};
let template_variables = match build_template_variables(&lockfile.config) {
Ok(variables) => variables,
Err(err) => {
warn!(error = %err, "failed to serialize resolved config for feature hint rendering");
return Vec::new();
}
};
FEATURES
.iter()
.filter(|spec| turn_context.features.enabled(spec.id))
.filter_map(|spec| {
let hint = features_toml.hint(spec.key)?;
let rendered = match render_feature_hint(hint, &template_variables) {
Ok(rendered) => rendered,
Err(err) => {
warn!(
feature = spec.key,
error = %err,
"failed to render feature hint"
);
return None;
}
};
if rendered.is_empty() {
return None;
}
build_developer_update_item(vec![rendered])
})
.collect()
}
fn render_feature_hint(
source: &str,
template_variables: &BTreeMap<String, String>,
) -> Result<String, TemplateError> {
let template = Template::parse(source)?;
let variables = template
.placeholders()
.map(|placeholder| {
template_variables
.get(placeholder)
.map(|value| (placeholder, value.as_str()))
.ok_or_else(|| TemplateRenderError::MissingValue {
name: placeholder.to_string(),
})
})
.collect::<Result<Vec<_>, _>>()?;
template.render(variables).map_err(Into::into)
}
fn build_template_variables(
config: &codex_config::config_toml::ConfigToml,
) -> Result<BTreeMap<String, String>, toml::ser::Error> {
let value = TomlValue::try_from(config)?;
let mut variables = BTreeMap::new();
flatten_toml_value(/*prefix*/ None, &value, &mut variables);
Ok(variables)
}
fn flatten_toml_value(
prefix: Option<&str>,
value: &TomlValue,
variables: &mut BTreeMap<String, String>,
) {
match value {
TomlValue::Table(table) => {
for (key, value) in table {
let key = match prefix {
Some(prefix) => format!("{prefix}.{key}"),
None => key.clone(),
};
flatten_toml_value(Some(&key), value, variables);
}
}
TomlValue::String(value) => insert_template_variable(prefix, value.clone(), variables),
TomlValue::Integer(value) => {
insert_template_variable(prefix, value.to_string(), variables);
}
TomlValue::Float(value) => {
insert_template_variable(prefix, value.to_string(), variables);
}
TomlValue::Boolean(value) => {
insert_template_variable(prefix, value.to_string(), variables);
}
TomlValue::Datetime(value) => {
insert_template_variable(prefix, value.to_string(), variables);
}
TomlValue::Array(value) => {
insert_template_variable(
prefix,
TomlValue::Array(value.clone()).to_string(),
variables,
);
}
}
}
fn insert_template_variable(
key: Option<&str>,
value: String,
variables: &mut BTreeMap<String, String>,
) {
if let Some(key) = key {
variables.insert(key.to_string(), value);
}
}
+7 -1
View File
@@ -184,6 +184,7 @@ use codex_protocol::error::Result as CodexResult;
use codex_protocol::exec_output::StreamOutput;
mod config_lock;
mod feature_hints;
mod handlers;
mod mcp;
mod multi_agents;
@@ -2540,6 +2541,7 @@ impl Session {
collaboration_mode,
base_instructions,
session_source,
session_configuration,
) = {
let state = self.state.lock().await;
(
@@ -2548,6 +2550,7 @@ impl Session {
state.session_configuration.collaboration_mode.clone(),
state.session_configuration.base_instructions.clone(),
state.session_configuration.session_source.clone(),
state.session_configuration.clone(),
)
};
if let Some(model_switch_message) =
@@ -2702,13 +2705,16 @@ impl Session {
let multi_agent_v2_usage_hint_text =
multi_agents::usage_hint_text(turn_context, &session_source);
let feature_hint_messages =
feature_hints::render_feature_hint_messages(&session_configuration, turn_context);
let mut items = Vec::with_capacity(4);
let mut items = Vec::with_capacity(4 + feature_hint_messages.len());
if let Some(developer_message) =
crate::context_manager::updates::build_developer_update_item(developer_sections)
{
items.push(developer_message);
}
items.extend(feature_hint_messages);
if let Some(usage_hint_text) = multi_agent_v2_usage_hint_text
&& let Some(usage_hint_message) =
crate::context_manager::updates::build_developer_update_item(vec![
+41
View File
@@ -5688,6 +5688,47 @@ async fn build_initial_context_omits_multi_agent_v2_usage_hints_when_feature_dis
);
}
#[tokio::test]
async fn build_initial_context_renders_feature_hint_developer_message_with_resolved_config_values()
{
let (session, turn_context, _rx_event) = make_session_and_context_with_auth_and_config_and_rx(
CodexAuth::from_api_key("Test API Key"),
Vec::new(),
|config| {
let _ = config.features.enable(Feature::MemoryTool);
config.agent_max_threads = Some(7);
let user_config_path = match config.config_layer_stack.get_user_layer() {
Some(layer) => match &layer.name {
codex_config::ConfigLayerSource::User { file } => file.clone(),
other => panic!("expected user config layer, got {other:?}"),
},
None => panic!("expected user config layer"),
};
config.config_layer_stack = config.config_layer_stack.with_user_config(
&user_config_path,
toml::toml! {
[features.memories]
enabled = true
hint = "This is a super hint {{ agents.max_threads }}"
}
.into(),
);
},
)
.await;
let initial_context = session.build_initial_context(turn_context.as_ref()).await;
let developer_messages = developer_message_texts(&initial_context);
assert!(
developer_messages
.iter()
.any(|message| message.as_slice() == ["This is a super hint 7"]),
"expected rendered feature hint developer message, got {developer_messages:?}"
);
}
#[tokio::test]
async fn configured_multi_agent_v2_usage_hint_texts_use_effective_enabled_feature_state() {
let (mut session, _turn_context) =