From e337e9b216b090cb0c97354270466a7fc059c514 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Sat, 2 May 2026 12:10:17 +0100 Subject: [PATCH] rendering --- codex-rs/core/src/session/config_lock.rs | 3 + codex-rs/core/src/session/feature_hints.rs | 136 +++++++++++++++++++++ codex-rs/core/src/session/mod.rs | 8 +- codex-rs/core/src/session/tests.rs | 41 +++++++ 4 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 codex-rs/core/src/session/feature_hints.rs diff --git a/codex-rs/core/src/session/config_lock.rs b/codex-rs/core/src/session/config_lock.rs index 40da3629de..8959cb2575 100644 --- a/codex-rs/core/src/session/config_lock.rs +++ b/codex-rs/core/src/session/config_lock.rs @@ -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; diff --git a/codex-rs/core/src/session/feature_hints.rs b/codex-rs/core/src/session/feature_hints.rs new file mode 100644 index 0000000000..4564213f0e --- /dev/null +++ b/codex-rs/core/src/session/feature_hints.rs @@ -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 { + 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, +) -> Result { + 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::, _>>()?; + template.render(variables).map_err(Into::into) +} + +fn build_template_variables( + config: &codex_config::config_toml::ConfigToml, +) -> Result, 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, +) { + 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, +) { + if let Some(key) = key { + variables.insert(key.to_string(), value); + } +} diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index c18976fde1..9a156241c6 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -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![ diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 89633daf35..11234bffe4 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -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) =