Emit host skill budget warnings from world state (#35766)

## Why

Host skill catalogs supplied through extension world state should report budget warnings only when their catalog update is actually rendered.

## What changed

- Stop rebuilding an extension-owned host catalog in core.
- Emit host catalog budget warnings when the corresponding world-state fragment is rendered, and deduplicate them with executor catalog warnings.
- Cover full, shortened, and omitted host and executor catalogs, including repeated turns and resumed persisted snapshots.

GitOrigin-RevId: ba28b6e337281b49ed4da2175c193fd4a6898ab7
This commit is contained in:
felixxia-oai authored and copyberry committed 2026-07-28 12:43:20 +00:00
1 parent e597169e9a
commit a68d0a74bd
5 files changed
+617 -22

No files matched your search

+24 -8
View File
@@ -63,6 +63,7 @@ use crate::state::SkillsThreadState;
use crate::state::SkillsTurnState;
use crate::tools::skill_tools;
use crate::warnings::bounded_warnings;
use crate::world_state::HostSkillsWarningEmitter;
use crate::world_state::executor_skills_world_state_section;
use crate::world_state::host_skills_world_state_section;
@@ -270,31 +271,46 @@ where
} else {
RenderedCatalog::default()
};
if let Some(message) = rendered.warning_message
&& input
.turn_store
.get_or_init(EmittedCatalogBudgetWarnings::default)
.insert(&message)
{
self.emit_warning(input.thread_store.level_id(), Some(input.turn_id), message);
}
let warning_message = rendered.warning_message;
let executor_body = rendered.fragment.map(|fragment| fragment.body());
let mut sections = vec![executor_skills_world_state_section(
executor_body,
config.include_instructions,
)];
let emitted_warnings = input
.turn_store
.get_or_init(EmittedCatalogBudgetWarnings::default);
if let Some(host_snapshot) = input.turn_store.get::<HostSkillsSnapshot>()
&& self.providers.has_host_provider()
{
input.turn_store.insert(HostSkillsCatalogInWorldState);
let event_sink = Arc::clone(&self.event_sink);
let emitted_warnings = Arc::clone(&emitted_warnings);
let thread_id = input.thread_store.level_id().to_string();
let turn_id = input.turn_id.to_string();
let warning_emitter: HostSkillsWarningEmitter = Arc::new(move |message| {
if emitted_warnings.insert(&message) {
event_sink.emit_warning(ExtensionWarning {
thread_id: thread_id.clone(),
turn_id: Some(turn_id.clone()),
message,
});
}
});
sections.push(host_skills_world_state_section(
&host_snapshot,
config.include_instructions,
include_usage,
metadata_budget,
extension_metrics,
warning_emitter,
));
}
if let Some(message) = warning_message
&& emitted_warnings.insert(&message)
{
self.emit_warning(input.thread_store.level_id(), Some(input.turn_id), message);
}
sections
})
}
+27
View File
@@ -26,6 +26,8 @@ const NO_HOST_SKILLS_BODY: &str =
"\n## Host skills update\nNo host skills are currently available.\n";
const HIDDEN_HOST_SKILLS_BODY: &str = "\n## Host skills update\nHost skills are not listed automatically. Explicit skill mentions can still be resolved when available.\n";
pub(crate) type HostSkillsWarningEmitter = Arc<dyn Fn(String) + Send + Sync>;
pub(crate) fn executor_skills_world_state_section(
body: Option<String>,
include_instructions: bool,
@@ -84,6 +86,7 @@ pub(crate) fn host_skills_world_state_section(
include_skills_usage_instructions: bool,
metadata_budget: SkillMetadataBudget,
extension_metrics: Option<Arc<dyn ExtensionMetrics>>,
warning_emitter: HostSkillsWarningEmitter,
) -> WorldStateSectionContribution {
let outcome = host_snapshot.outcome();
let metadata_budget = match metadata_budget {
@@ -97,6 +100,9 @@ pub(crate) fn host_skills_world_state_section(
} else {
None
};
let warning_message = available
.as_ref()
.and_then(|available| available.warning_message.clone());
let render_metrics = include_instructions.then(|| {
available
.as_ref()
@@ -118,6 +124,24 @@ pub(crate) fn host_skills_world_state_section(
)
.body()
});
rendered_host_skills_world_state_section(
body,
include_instructions,
render_metrics,
extension_metrics,
warning_message,
warning_emitter,
)
}
pub(crate) fn rendered_host_skills_world_state_section(
body: Option<String>,
include_instructions: bool,
render_metrics: Option<(usize, usize, usize, usize)>,
extension_metrics: Option<Arc<dyn ExtensionMetrics>>,
warning_message: Option<String>,
warning_emitter: HostSkillsWarningEmitter,
) -> WorldStateSectionContribution {
let snapshot = json!({
"body": body,
"includeInstructions": include_instructions,
@@ -159,6 +183,9 @@ pub(crate) fn host_skills_world_state_section(
None if !include_instructions => HIDDEN_HOST_SKILLS_BODY,
None => NO_HOST_SKILLS_BODY,
};
if let Some(message) = warning_message.as_ref() {
warning_emitter(message.clone());
}
Some(RenderedWorldStateFragment::new(
"developer",
(SKILLS_INSTRUCTIONS_OPEN_TAG, SKILLS_INSTRUCTIONS_CLOSE_TAG),