mirror of
https://github.com/openai/codex.git
synced 2026-11-01 19:33:35 +00:00
Move skill catalog rendering out of core (#36364)
## What changed - Make the skills extension own the catalog prompt templates and rendering path. - Remove the duplicate core fallback that injected available skills into initial context. - Update core integration tests to install the skills extension explicitly when they expect model-visible skill context. GitOrigin-RevId: bbe6b15c4e776a09c98b7f4166426fe4e58434ba
This commit is contained in:
@@ -18,20 +18,17 @@ use crate::agent_communication::AgentCommunicationContext;
|
||||
use crate::agent_communication::AgentCommunicationKind;
|
||||
use crate::attestation::AttestationProvider;
|
||||
use crate::audio_preparation::prepare_response_items as prepare_audio_response_items;
|
||||
use crate::build_available_skills;
|
||||
use crate::compact;
|
||||
use crate::compact::CompactedHistoryMetadata;
|
||||
use crate::config::ManagedFeatures;
|
||||
use crate::config::resolve_tool_suggest_config_from_layer_stack;
|
||||
use crate::context::ApprovedCommandPrefixSaved;
|
||||
use crate::context::AvailableSkillsInstructions;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::ModelSwitchInstructions;
|
||||
use crate::context::NetworkRuleSaved;
|
||||
use crate::context::RecommendedPluginsInstructions;
|
||||
use crate::context::world_state::WorldState;
|
||||
use crate::current_time::TimeProvider;
|
||||
use crate::default_skill_metadata_budget;
|
||||
use crate::environment_selection::TurnEnvironmentSnapshot;
|
||||
use crate::exec_policy::BANNED_PREFIX_SUGGESTIONS;
|
||||
use crate::exec_policy::ExecPolicyManager;
|
||||
@@ -42,7 +39,6 @@ use crate::realtime_conversation::RealtimeConversationManager;
|
||||
use crate::session::step_context::StepContext;
|
||||
use crate::session::turn_context::TurnEnvironment;
|
||||
use crate::session_prefix::format_inter_agent_completion_message;
|
||||
use crate::skills::SkillRenderSideEffects;
|
||||
use crate::skills_load_input_from_config;
|
||||
use crate::turn_metadata::TurnMetadataState;
|
||||
use crate::turn_timing::now_unix_timestamp_ms;
|
||||
@@ -55,7 +51,6 @@ use codex_analytics::SubAgentThreadStartedInput;
|
||||
use codex_analytics::TurnCodexErrorFact;
|
||||
use codex_async_utils::OrCancelExt;
|
||||
use codex_connectors::connector_runtime_context_key;
|
||||
use codex_core_skills::injection::HostSkillsCatalogInWorldState;
|
||||
use codex_exec_server::Environment;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_execpolicy::prefix_rule_migration;
|
||||
@@ -304,8 +299,6 @@ pub(crate) struct PreviousTurnSettings {
|
||||
pub(crate) realtime_active: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
use crate::SkillMetadata;
|
||||
use crate::SkillsService;
|
||||
use crate::exec_policy::ExecPolicyUpdateError;
|
||||
use crate::guardian::GuardianReviewOptions;
|
||||
@@ -315,8 +308,6 @@ use crate::network_policy_decision::execpolicy_network_rule_amendment;
|
||||
use crate::rollout::map_session_init_error;
|
||||
use crate::session_startup_prewarm::SessionStartupPrewarmHandle;
|
||||
use crate::shell;
|
||||
#[cfg(test)]
|
||||
use crate::skills::SkillLoadOutcome;
|
||||
use crate::state::AutoCompactWindowIds;
|
||||
use crate::state::AutoCompactWindowSnapshot;
|
||||
use crate::state::PendingRequestPermissions;
|
||||
@@ -3410,37 +3401,6 @@ impl Session {
|
||||
{
|
||||
developer_sections.push(developer_instructions.to_string());
|
||||
}
|
||||
if turn_context.config.include_skill_instructions
|
||||
&& turn_context
|
||||
.extension_data
|
||||
.get::<HostSkillsCatalogInWorldState>()
|
||||
.is_none()
|
||||
{
|
||||
let available_skills = build_available_skills(
|
||||
turn_context.turn_skills.snapshot.outcome(),
|
||||
default_skill_metadata_budget(turn_context.model_info.context_window),
|
||||
SkillRenderSideEffects::ThreadStart {
|
||||
session_telemetry: &self.services.session_telemetry,
|
||||
},
|
||||
);
|
||||
if let Some(available_skills) = available_skills {
|
||||
let warning_message = available_skills.warning_message.clone();
|
||||
let skills_instructions = AvailableSkillsInstructions::from_available_skills(
|
||||
&available_skills,
|
||||
turn_context.model_info.include_skills_usage_instructions,
|
||||
);
|
||||
if let Some(warning_message) = warning_message {
|
||||
self.send_event_raw(Event {
|
||||
id: String::new(),
|
||||
msg: EventMsg::Warning(WarningEvent {
|
||||
message: warning_message,
|
||||
}),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
developer_sections.push(skills_instructions.render());
|
||||
}
|
||||
}
|
||||
let loaded_plugins = self
|
||||
.services
|
||||
.plugins_manager
|
||||
|
||||
@@ -14,8 +14,6 @@ use crate::function_tool::FunctionCallError;
|
||||
use crate::session::step_context::StepContext;
|
||||
use crate::shell::default_user_shell;
|
||||
use crate::shell_snapshot::ShellSnapshot;
|
||||
use crate::skills::SkillRenderSideEffects;
|
||||
use crate::skills::render::SkillMetadataBudget;
|
||||
use crate::test_support::models_manager_with_provider;
|
||||
use crate::tools::format_exec_output_str;
|
||||
use crate::tools::registry::CoreToolRuntime;
|
||||
@@ -33,7 +31,6 @@ use codex_config::loader::project_trust_key;
|
||||
use codex_config::types::McpServerConfig;
|
||||
use codex_config::types::McpServerTransportConfig;
|
||||
use codex_config::types::ToolSuggestDisabledTool;
|
||||
use codex_core_skills::HostSkillsSnapshot;
|
||||
use core_test_support::test_codex::local_selections;
|
||||
|
||||
use codex_features::Feature;
|
||||
@@ -109,10 +106,6 @@ use codex_execpolicy::Policy;
|
||||
use codex_network_proxy::NetworkProxyConfig;
|
||||
use codex_otel::MetricsClient;
|
||||
use codex_otel::MetricsConfig;
|
||||
use codex_otel::THREAD_SKILLS_DESCRIPTION_TRUNCATED_CHARS_METRIC;
|
||||
use codex_otel::THREAD_SKILLS_ENABLED_TOTAL_METRIC;
|
||||
use codex_otel::THREAD_SKILLS_KEPT_TOTAL_METRIC;
|
||||
use codex_otel::THREAD_SKILLS_TRUNCATED_METRIC;
|
||||
use codex_otel::TelemetryAuthMode;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
@@ -143,7 +136,6 @@ use codex_protocol::protocol::ResumedHistory;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::SessionMeta;
|
||||
use codex_protocol::protocol::SessionMetaLine;
|
||||
use codex_protocol::protocol::SkillScope;
|
||||
use codex_protocol::protocol::Submission;
|
||||
use codex_protocol::protocol::ThreadRolledBackEvent;
|
||||
use codex_protocol::protocol::ThreadSettingsOverrides;
|
||||
@@ -318,28 +310,6 @@ fn assistant_message(text: &str) -> ResponseItem {
|
||||
}
|
||||
}
|
||||
|
||||
fn test_session_telemetry_without_metadata() -> SessionTelemetry {
|
||||
let exporter = InMemoryMetricExporter::default();
|
||||
let metrics = MetricsClient::new(
|
||||
MetricsConfig::in_memory("test", "codex-core", env!("CARGO_PKG_VERSION"), exporter)
|
||||
.with_runtime_reader(),
|
||||
)
|
||||
.expect("in-memory metrics client");
|
||||
SessionTelemetry::new(
|
||||
ThreadId::new(),
|
||||
"gpt-5.4",
|
||||
"gpt-5.4",
|
||||
/*account_id*/ None,
|
||||
/*account_email*/ None,
|
||||
/*auth_mode*/ None,
|
||||
"test_originator".to_string(),
|
||||
/*log_user_prompts*/ false,
|
||||
"tty".to_string(),
|
||||
SessionSource::Cli,
|
||||
)
|
||||
.with_metrics_without_metadata_tags(metrics)
|
||||
}
|
||||
|
||||
fn find_metric<'a>(resource_metrics: &'a ResourceMetrics, name: &str) -> &'a Metric {
|
||||
for scope_metrics in resource_metrics.scope_metrics() {
|
||||
for metric in scope_metrics.metrics() {
|
||||
@@ -351,21 +321,6 @@ fn find_metric<'a>(resource_metrics: &'a ResourceMetrics, name: &str) -> &'a Met
|
||||
panic!("metric {name} missing");
|
||||
}
|
||||
|
||||
fn histogram_sum(resource_metrics: &ResourceMetrics, name: &str) -> u64 {
|
||||
let metric = find_metric(resource_metrics, name);
|
||||
match metric.data() {
|
||||
AggregatedMetrics::F64(data) => match data {
|
||||
MetricData::Histogram(histogram) => {
|
||||
let points: Vec<_> = histogram.data_points().collect();
|
||||
assert_eq!(points.len(), 1);
|
||||
points[0].sum().round() as u64
|
||||
}
|
||||
_ => panic!("unexpected histogram aggregation"),
|
||||
},
|
||||
_ => panic!("unexpected metric data type"),
|
||||
}
|
||||
}
|
||||
|
||||
fn single_histogram_attributes(
|
||||
resource_metrics: &ResourceMetrics,
|
||||
name: &str,
|
||||
@@ -9168,220 +9123,6 @@ async fn build_initial_context_omits_multi_agent_v2_usage_hints_when_hint_is_emp
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_initial_context_trims_skill_metadata_from_context_window_budget() {
|
||||
let (session, mut turn_context) = make_session_and_context().await;
|
||||
let mut outcome = SkillLoadOutcome::default();
|
||||
outcome.skills = vec![
|
||||
SkillMetadata {
|
||||
name: "admin-skill".to_string(),
|
||||
description: "desc".to_string(),
|
||||
short_description: None,
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
path_to_skills_md: test_path_buf("/tmp/admin-skill/SKILL.md").abs(),
|
||||
scope: SkillScope::Admin,
|
||||
plugin_id: None,
|
||||
remote_plugin_id: None,
|
||||
},
|
||||
SkillMetadata {
|
||||
name: "repo-skill".to_string(),
|
||||
description: "desc".to_string(),
|
||||
short_description: None,
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
path_to_skills_md: test_path_buf("/tmp/repo-skill/SKILL.md").abs(),
|
||||
scope: SkillScope::Repo,
|
||||
plugin_id: None,
|
||||
remote_plugin_id: None,
|
||||
},
|
||||
];
|
||||
turn_context.model_info.context_window = Some(100);
|
||||
turn_context.turn_skills = TurnSkillsContext::new(HostSkillsSnapshot::new(Arc::new(outcome)));
|
||||
let turn_context = Arc::new(turn_context);
|
||||
|
||||
let initial_context = build_initial_context(&session, &turn_context).await;
|
||||
let developer_texts = developer_input_texts(&initial_context);
|
||||
|
||||
assert!(
|
||||
developer_texts
|
||||
.iter()
|
||||
.all(|text| !text.contains("Exceeded skills context budget")),
|
||||
"expected skill budget warning to stay out of the initial context, got {developer_texts:?}"
|
||||
);
|
||||
assert!(
|
||||
developer_texts
|
||||
.iter()
|
||||
.all(|text| !text.contains("- admin-skill:") && !text.contains("- repo-skill:")),
|
||||
"expected no skill metadata entries to fit the tiny budget, got {developer_texts:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_thread_start_skill_metrics_records_enabled_kept_and_truncated_values() {
|
||||
let session_telemetry = test_session_telemetry_without_metadata();
|
||||
let mut outcome = SkillLoadOutcome::default();
|
||||
outcome.skills = vec![SkillMetadata {
|
||||
name: "repo-skill".to_string(),
|
||||
description: "desc".to_string(),
|
||||
short_description: None,
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
path_to_skills_md: test_path_buf("/tmp/repo-skill/SKILL.md").abs(),
|
||||
scope: SkillScope::Repo,
|
||||
plugin_id: None,
|
||||
remote_plugin_id: None,
|
||||
}];
|
||||
let rendered = build_available_skills(
|
||||
&outcome,
|
||||
SkillMetadataBudget::Characters(1),
|
||||
SkillRenderSideEffects::ThreadStart {
|
||||
session_telemetry: &session_telemetry,
|
||||
},
|
||||
)
|
||||
.expect("skills should render");
|
||||
|
||||
assert_eq!(
|
||||
rendered.warning_message,
|
||||
Some(
|
||||
"Exceeded skills context budget. All skill descriptions were removed and 1 additional skill was not included in the model-visible skills list."
|
||||
.to_string()
|
||||
)
|
||||
);
|
||||
let snapshot = session_telemetry
|
||||
.snapshot_metrics()
|
||||
.expect("runtime metrics snapshot");
|
||||
assert_eq!(
|
||||
histogram_sum(&snapshot, THREAD_SKILLS_ENABLED_TOTAL_METRIC),
|
||||
1
|
||||
);
|
||||
assert_eq!(histogram_sum(&snapshot, THREAD_SKILLS_KEPT_TOTAL_METRIC), 0);
|
||||
assert_eq!(histogram_sum(&snapshot, THREAD_SKILLS_TRUNCATED_METRIC), 1);
|
||||
assert_eq!(
|
||||
histogram_sum(&snapshot, THREAD_SKILLS_DESCRIPTION_TRUNCATED_CHARS_METRIC),
|
||||
4
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_thread_start_skill_metrics_records_description_truncated_chars_without_omitted_skills() {
|
||||
let session_telemetry = test_session_telemetry_without_metadata();
|
||||
let alpha = SkillMetadata {
|
||||
name: "alpha-skill".to_string(),
|
||||
description: "abcdef".to_string(),
|
||||
short_description: None,
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
path_to_skills_md: test_path_buf("/tmp/alpha-skill/SKILL.md").abs(),
|
||||
scope: SkillScope::Repo,
|
||||
plugin_id: None,
|
||||
remote_plugin_id: None,
|
||||
};
|
||||
let beta = SkillMetadata {
|
||||
name: "beta-skill".to_string(),
|
||||
description: "uvwxyz".to_string(),
|
||||
short_description: None,
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
path_to_skills_md: test_path_buf("/tmp/beta-skill/SKILL.md").abs(),
|
||||
scope: SkillScope::Repo,
|
||||
plugin_id: None,
|
||||
remote_plugin_id: None,
|
||||
};
|
||||
let minimum_skill_line_cost = |skill: &SkillMetadata| {
|
||||
let path = skill.path_to_skills_md.to_string_lossy().replace('\\', "/");
|
||||
format!("- {}: (file: {})\n", skill.name, path)
|
||||
.chars()
|
||||
.count()
|
||||
};
|
||||
let minimum_budget = minimum_skill_line_cost(&alpha) + minimum_skill_line_cost(&beta);
|
||||
let mut outcome = SkillLoadOutcome::default();
|
||||
outcome.skills = vec![alpha, beta];
|
||||
|
||||
let rendered = build_available_skills(
|
||||
&outcome,
|
||||
SkillMetadataBudget::Characters(minimum_budget + 6),
|
||||
SkillRenderSideEffects::ThreadStart {
|
||||
session_telemetry: &session_telemetry,
|
||||
},
|
||||
)
|
||||
.expect("skills should render");
|
||||
|
||||
assert_eq!(rendered.report.omitted_count, 0);
|
||||
assert_eq!(rendered.report.truncated_description_chars, 8);
|
||||
let snapshot = session_telemetry
|
||||
.snapshot_metrics()
|
||||
.expect("runtime metrics snapshot");
|
||||
assert_eq!(histogram_sum(&snapshot, THREAD_SKILLS_TRUNCATED_METRIC), 0);
|
||||
assert_eq!(
|
||||
histogram_sum(&snapshot, THREAD_SKILLS_DESCRIPTION_TRUNCATED_CHARS_METRIC),
|
||||
8
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_initial_context_emits_thread_start_skill_warning_on_repeated_builds() {
|
||||
let (session, turn_context, rx) = make_session_and_context_with_rx().await;
|
||||
let mut turn_context = Arc::into_inner(turn_context).expect("sole thread settings owner");
|
||||
let mut outcome = SkillLoadOutcome::default();
|
||||
outcome.skills = vec![
|
||||
SkillMetadata {
|
||||
name: "admin-skill".to_string(),
|
||||
description: "desc".to_string(),
|
||||
short_description: None,
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
path_to_skills_md: test_path_buf("/tmp/admin-skill/SKILL.md").abs(),
|
||||
scope: SkillScope::Admin,
|
||||
plugin_id: None,
|
||||
remote_plugin_id: None,
|
||||
},
|
||||
SkillMetadata {
|
||||
name: "repo-skill".to_string(),
|
||||
description: "desc".to_string(),
|
||||
short_description: None,
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
path_to_skills_md: test_path_buf("/tmp/repo-skill/SKILL.md").abs(),
|
||||
scope: SkillScope::Repo,
|
||||
plugin_id: None,
|
||||
remote_plugin_id: None,
|
||||
},
|
||||
];
|
||||
turn_context.model_info.context_window = Some(100);
|
||||
turn_context.turn_skills = TurnSkillsContext::new(HostSkillsSnapshot::new(Arc::new(outcome)));
|
||||
let turn_context = Arc::new(turn_context);
|
||||
|
||||
let _ = build_initial_context(&session, &turn_context).await;
|
||||
let warning_event = timeout(Duration::from_secs(1), rx.recv())
|
||||
.await
|
||||
.expect("warning event should arrive")
|
||||
.expect("warning event should be readable");
|
||||
assert!(matches!(
|
||||
warning_event.msg,
|
||||
EventMsg::Warning(WarningEvent { message })
|
||||
if message == "Exceeded skills context budget of 2%. All skill descriptions were removed and 2 additional skills were not included in the model-visible skills list."
|
||||
));
|
||||
|
||||
let _ = build_initial_context(&session, &turn_context).await;
|
||||
let warning_event = timeout(Duration::from_secs(1), rx.recv())
|
||||
.await
|
||||
.expect("warning event should arrive on repeated build")
|
||||
.expect("warning event should be readable");
|
||||
assert!(matches!(
|
||||
warning_event.msg,
|
||||
EventMsg::Warning(WarningEvent { message })
|
||||
if message == "Exceeded skills context budget of 2%. All skill descriptions were removed and 2 additional skills were not included in the model-visible skills list."
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_initial_context_restates_realtime_start_when_reference_context_is_missing() {
|
||||
let (session, mut turn_context) = make_session_and_context().await;
|
||||
|
||||
Reference in New Issue
Block a user