Scale skill metadata budgets with model context windows (#34626)

## Why

A fixed character limit does not account for the different context-window sizes supported by models.

## What changed

- Budget extension-rendered skill metadata at 2% of the resolved model context window, capped at 4,000 tokens.
- Keep the existing 8,000-character fallback when model context metadata is unavailable.
- Include the omission marker in the budget and still emit it when no skill entry fits.
- Apply the same resolved budget to executor and host skill catalogs assembled for a turn.

## Testing

- Cover proportional and capped budgets, multibyte fallback accounting, and omission-marker behavior.
- Verify through the production turn path that larger model context windows include more catalog entries without exceeding the computed budget.

GitOrigin-RevId: 4667293f1594de4dd605b32b9fa4255d26772c0f
This commit is contained in:
felixxia-oai authored and copyberry committed 2026-07-21 22:16:27 +00:00
1 parent bbad09a83b
commit 37eef7bacc
10 files changed
+454 -31

No files matched your search

+16 -8
View File
@@ -1,7 +1,6 @@
use std::sync::Arc;
use codex_core_skills::HostSkillsSnapshot;
use codex_core_skills::default_skill_metadata_budget;
use codex_core_skills::injection::HostSkillsCatalogInWorldState;
use codex_core_skills::injection::InjectedHostSkillPrompts;
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
@@ -45,6 +44,7 @@ use crate::render::MAX_SKILL_NAME_BYTES;
use crate::render::MAX_SKILL_PATH_BYTES;
use crate::render::SkillCatalogRenderPolicy;
use crate::render::available_skills_fragment;
use crate::render::capped_skill_metadata_budget;
use crate::render::truncate_main_prompt_contents;
use crate::render::truncate_utf8_to_bytes;
use crate::selection::collect_explicit_skill_mentions;
@@ -149,6 +149,7 @@ where
&catalog,
include_usage,
SkillCatalogRenderPolicy::ExtensionCompatible,
capped_skill_metadata_budget(/*context_window*/ None),
)
.map(|fragment| PromptFragment::developer_capability(fragment.render()))
.into_iter()
@@ -187,10 +188,15 @@ where
let include_usage = model_info
.as_deref()
.is_some_and(|model_info| model_info.include_skills_usage_instructions);
let context_window = model_info
.as_deref()
.and_then(ModelInfo::resolved_context_window);
let metadata_budget = capped_skill_metadata_budget(context_window);
let mut sections = vec![executor_skills_world_state_section(
&catalog,
config.include_instructions,
include_usage,
metadata_budget,
)];
if let Some(host_snapshot) = input.turn_store.get::<HostSkillsSnapshot>()
&& self.providers.has_host_provider()
@@ -200,11 +206,7 @@ where
&host_snapshot,
config.include_instructions,
include_usage,
default_skill_metadata_budget(
model_info
.as_deref()
.and_then(|model_info| model_info.context_window),
),
metadata_budget,
));
}
sections
@@ -331,13 +333,19 @@ where
entry.authority.kind != SkillSourceKind::Executor
&& entry.authority.kind != SkillSourceKind::Orchestrator
});
let include_usage = thread_store
.get::<ModelInfo>()
let model_info = thread_store.get::<ModelInfo>();
let include_usage = model_info
.as_deref()
.is_some_and(|model_info| model_info.include_skills_usage_instructions);
let context_window = model_info
.as_deref()
.and_then(ModelInfo::resolved_context_window);
let metadata_budget = capped_skill_metadata_budget(context_window);
if let Some(fragment) = available_skills_fragment(
&turn_catalog,
include_usage,
SkillCatalogRenderPolicy::ExtensionCompatible,
metadata_budget,
) {
fragments.push(Box::new(fragment));
}
+64 -16
View File
@@ -1,5 +1,6 @@
use std::borrow::Cow;
use codex_utils_string::approx_token_count;
use codex_utils_string::take_bytes_at_char_boundary;
use crate::catalog::SkillCatalog;
@@ -7,7 +8,9 @@ use crate::catalog::SkillCatalogEntry;
use crate::catalog::SkillSourceKind;
use crate::fragments::AvailableSkillsInstructions;
const MAX_AVAILABLE_SKILLS_BYTES: usize = 8_000;
const DEFAULT_SKILL_METADATA_CHAR_BUDGET: usize = 8_000;
const MAX_SKILL_METADATA_TOKEN_BUDGET: usize = 4_000;
const SKILL_METADATA_CONTEXT_WINDOW_PERCENT: usize = 2;
const MAX_MAIN_PROMPT_BYTES: usize = 8_000;
const MAX_CATALOG_SKILL_DESCRIPTION_CHARS: usize = 1_024;
const TRUNCATED_SKILL_DESCRIPTION_SUFFIX: &str = "...";
@@ -39,6 +42,37 @@ impl SkillCatalogRenderPolicy {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SkillMetadataBudget {
Tokens(usize),
Characters(usize),
}
pub(crate) fn capped_skill_metadata_budget(context_window: Option<i64>) -> SkillMetadataBudget {
context_window
.and_then(|window| usize::try_from(window).ok())
.filter(|window| *window > 0)
.map(|window| {
SkillMetadataBudget::Tokens(
window
.saturating_mul(SKILL_METADATA_CONTEXT_WINDOW_PERCENT)
.saturating_div(100)
.clamp(1, MAX_SKILL_METADATA_TOKEN_BUDGET),
)
})
.unwrap_or(SkillMetadataBudget::Characters(
DEFAULT_SKILL_METADATA_CHAR_BUDGET,
))
}
fn metadata_line_cost(budget: SkillMetadataBudget, line: &str) -> usize {
let line = format!("{line}\n");
match budget {
SkillMetadataBudget::Tokens(_) => approx_token_count(&line),
SkillMetadataBudget::Characters(_) => line.chars().count(),
}
}
#[tracing::instrument(
level = "trace",
skip_all,
@@ -48,8 +82,12 @@ pub(crate) fn available_skills_fragment(
catalog: &SkillCatalog,
include_skills_usage_instructions: bool,
policy: SkillCatalogRenderPolicy,
budget: SkillMetadataBudget,
) -> Option<AvailableSkillsInstructions> {
let mut total_bytes = 0usize;
let budget_limit = match budget {
SkillMetadataBudget::Tokens(limit) | SkillMetadataBudget::Characters(limit) => limit,
};
let mut total_cost = 0usize;
let mut omitted = 0usize;
let mut skill_lines = Vec::new();
@@ -61,29 +99,39 @@ pub(crate) fn available_skills_fragment(
let description = policy.description(entry);
let description = truncate_catalog_skill_description(description);
let line = render_skill_line(entry, description.as_ref());
let next_bytes = total_bytes.saturating_add(line.len());
if next_bytes > MAX_AVAILABLE_SKILLS_BYTES {
let next_cost = total_cost.saturating_add(metadata_line_cost(budget, &line));
if next_cost > budget_limit {
omitted = omitted.saturating_add(1);
continue;
}
total_bytes = next_bytes;
total_cost = next_cost;
skill_lines.push(line);
}
if skill_lines.is_empty() {
return None;
}
if omitted > 0 {
let skill_word = if omitted == 1 { "skill" } else { "skills" };
skill_lines.push(format!(
"- {omitted} additional {skill_word} omitted from this bounded skills list."
));
loop {
let marker = omission_marker(omitted);
if total_cost.saturating_add(metadata_line_cost(budget, &marker)) <= budget_limit {
skill_lines.push(marker);
break;
}
let line = skill_lines.pop()?;
total_cost = total_cost.saturating_sub(metadata_line_cost(budget, &line));
omitted = omitted.saturating_add(1);
}
}
Some(AvailableSkillsInstructions::from_skill_lines(
skill_lines,
include_skills_usage_instructions,
))
(!skill_lines.is_empty()).then(|| {
AvailableSkillsInstructions::from_skill_lines(
skill_lines,
include_skills_usage_instructions,
)
})
}
fn omission_marker(omitted: usize) -> String {
let skill_word = if omitted == 1 { "skill" } else { "skills" };
format!("- {omitted} additional {skill_word} omitted from this bounded skills list.")
}
pub(crate) fn truncate_catalog_skill_description(description: &str) -> Cow<'_, str> {
+109
View File
@@ -35,12 +35,14 @@ fn description_selection_follows_render_policy() {
&catalog,
/*include_skills_usage_instructions*/ false,
SkillCatalogRenderPolicy::CoreCompatible,
SkillMetadataBudget::Characters(8_000),
)
.expect("catalog should render");
let extension = available_skills_fragment(
&catalog,
/*include_skills_usage_instructions*/ false,
SkillCatalogRenderPolicy::ExtensionCompatible,
SkillMetadataBudget::Characters(8_000),
)
.expect("catalog should render");
@@ -65,3 +67,110 @@ fn description_selection_follows_render_policy() {
)
);
}
#[test]
fn catalog_budget_uses_capped_context_percentage_or_character_fallback() {
assert_eq!(
capped_skill_metadata_budget(Some(100_000)),
SkillMetadataBudget::Tokens(2_000)
);
assert_eq!(
capped_skill_metadata_budget(Some(400_000)),
SkillMetadataBudget::Tokens(4_000)
);
assert_eq!(
capped_skill_metadata_budget(/*context_window*/ None),
SkillMetadataBudget::Characters(8_000)
);
}
#[test]
fn omission_marker_is_charged_to_catalog_budget() {
let catalog = SkillCatalog {
entries: (0..20)
.map(|index| {
entry(
&format!("skill-{index:02}"),
"A description long enough to put the catalog under budget pressure.",
/*short_description*/ None,
)
})
.collect(),
warnings: Vec::new(),
};
let fragment = available_skills_fragment(
&catalog,
/*include_skills_usage_instructions*/ false,
SkillCatalogRenderPolicy::ExtensionCompatible,
SkillMetadataBudget::Tokens(100),
)
.expect("catalog should render");
let rendered_metadata_cost = fragment
.body()
.lines()
.filter(|line| line.starts_with("- "))
.map(|line| approx_token_count(&format!("{line}\n")))
.sum::<usize>();
assert!(fragment.body().contains("additional skills omitted"));
assert!(rendered_metadata_cost <= 100);
}
#[test]
fn character_fallback_counts_multibyte_metadata_by_characters() {
let description = "💡".repeat(MAX_CATALOG_SKILL_DESCRIPTION_CHARS);
let catalog = SkillCatalog {
entries: vec![
entry(
"multibyte-one",
&description,
/*short_description*/ None,
),
entry(
"multibyte-two",
&description,
/*short_description*/ None,
),
],
warnings: Vec::new(),
};
let fragment = available_skills_fragment(
&catalog,
/*include_skills_usage_instructions*/ false,
SkillCatalogRenderPolicy::ExtensionCompatible,
SkillMetadataBudget::Characters(8_000),
)
.expect("catalog should render");
assert!(fragment.body().contains("multibyte-one"));
assert!(fragment.body().contains("multibyte-two"));
assert!(!fragment.body().contains("additional skills omitted"));
}
#[test]
fn catalog_emits_omission_marker_when_every_skill_exceeds_budget() {
let catalog = SkillCatalog {
entries: vec![entry(
"oversized",
&"x".repeat(MAX_CATALOG_SKILL_DESCRIPTION_CHARS),
/*short_description*/ None,
)],
warnings: Vec::new(),
};
let fragment = available_skills_fragment(
&catalog,
/*include_skills_usage_instructions*/ false,
SkillCatalogRenderPolicy::ExtensionCompatible,
SkillMetadataBudget::Tokens(100),
)
.expect("omission marker should fit");
assert!(!fragment.body().contains("- oversized:"));
assert!(
fragment
.body()
.contains("- 1 additional skill omitted from this bounded skills list.")
);
}
+5 -7
View File
@@ -1,5 +1,4 @@
use codex_core_skills::HostSkillsSnapshot;
use codex_core_skills::SkillMetadataBudget;
use codex_core_skills::build_available_skills;
use codex_core_skills::render::SkillRenderSideEffects;
use codex_extension_api::ContextualUserFragment;
@@ -13,12 +12,11 @@ use serde_json::json;
use crate::catalog::SkillCatalog;
use crate::fragments::AvailableSkillsInstructions;
use crate::render::SkillCatalogRenderPolicy;
use crate::render::SkillMetadataBudget;
use crate::render::available_skills_fragment;
pub(crate) const SKILLS_WORLD_STATE_ID: &str = "skills";
pub(crate) const HOST_SKILLS_WORLD_STATE_ID: &str = "host_skills";
const MAX_HOST_SKILLS_METADATA_CHARS: usize = 8_000;
const MAX_HOST_SKILLS_METADATA_TOKENS: usize = 4_000;
const NO_EXECUTOR_SKILLS_BODY: &str =
"\n## Skills update\nNo selected-environment skills are currently available.\n";
const HIDDEN_EXECUTOR_SKILLS_BODY: &str = "\n## Skills update\nSelected-environment skills are not listed automatically. Explicit skill mentions can still be resolved when available.\n";
@@ -30,12 +28,14 @@ pub(crate) fn executor_skills_world_state_section(
catalog: &SkillCatalog,
include_instructions: bool,
include_skills_usage_instructions: bool,
metadata_budget: SkillMetadataBudget,
) -> WorldStateSectionContribution {
let body = if include_instructions {
available_skills_fragment(
catalog,
include_skills_usage_instructions,
SkillCatalogRenderPolicy::ExtensionCompatible,
metadata_budget,
)
.map(|fragment| fragment.body())
} else {
@@ -95,11 +95,9 @@ pub(crate) fn host_skills_world_state_section(
) -> WorldStateSectionContribution {
let outcome = host_snapshot.outcome();
let metadata_budget = match metadata_budget {
SkillMetadataBudget::Tokens(limit) => {
SkillMetadataBudget::Tokens(limit.min(MAX_HOST_SKILLS_METADATA_TOKENS))
}
SkillMetadataBudget::Tokens(limit) => codex_core_skills::SkillMetadataBudget::Tokens(limit),
SkillMetadataBudget::Characters(limit) => {
SkillMetadataBudget::Characters(limit.min(MAX_HOST_SKILLS_METADATA_CHARS))
codex_core_skills::SkillMetadataBudget::Characters(limit)
}
};
let available = if include_instructions {