mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Route extension warnings to app-server threads (#35168)
## Why Extension warnings need a stable thread target even when they are emitted outside an active turn. ## What changed - Add `ExtensionWarning` and a dedicated `ExtensionEventSink::emit_warning` path with an explicit thread ID and optional turn ID. - Deliver extension warnings as thread-scoped app-server `warning` notifications, preserving listener ordering and waiting briefly for a subscriber when no listener is active. - Move skills warnings to the new path, limiting provider catalog warnings to four messages of 256 bytes each. ## Testing - Cover warning routing, ordering, UTF-8-safe truncation, subscriber fallback, and invalid thread IDs. - Verify skills warning bounds and app-server delivery of executor catalog budget warnings. GitOrigin-RevId: 4dcb1ba7f17c0edb1a9d548f9568e0c734bffc15
This commit is contained in:
1 parent
c8957bbf0f
commit
5dd992acd3
15 files changed
+592
-65
No files matched your search
@@ -11,6 +11,7 @@ use codex_extension_api::ExtensionData;
|
||||
use codex_extension_api::ExtensionEventSink;
|
||||
use codex_extension_api::ExtensionFuture;
|
||||
use codex_extension_api::ExtensionRegistryBuilder;
|
||||
use codex_extension_api::ExtensionWarning;
|
||||
use codex_extension_api::PromptFragment;
|
||||
use codex_extension_api::SkillInvocationContributor;
|
||||
use codex_extension_api::SkillInvocationInput;
|
||||
@@ -27,9 +28,6 @@ use codex_extension_api::WorldStateSectionContribution;
|
||||
use codex_mcp::McpResourceClient;
|
||||
use codex_otel::MetricsClient;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::protocol::Event;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::WarningEvent;
|
||||
|
||||
use crate::SkillsExtensionConfig;
|
||||
use crate::catalog::SkillCatalog;
|
||||
@@ -58,6 +56,7 @@ use crate::state::SkillsSessionState;
|
||||
use crate::state::SkillsThreadState;
|
||||
use crate::state::SkillsTurnState;
|
||||
use crate::tools::skill_tools;
|
||||
use crate::warnings::bounded_warnings;
|
||||
use crate::world_state::executor_skills_world_state_section;
|
||||
use crate::world_state::host_skills_world_state_section;
|
||||
|
||||
@@ -170,8 +169,8 @@ where
|
||||
&thread_state,
|
||||
)
|
||||
.await;
|
||||
for warning in &catalog.warnings {
|
||||
self.emit_warning(thread_store.level_id(), warning.clone());
|
||||
for warning in bounded_warnings(&catalog.warnings) {
|
||||
self.emit_warning(thread_store.level_id(), /*turn_id*/ None, warning);
|
||||
}
|
||||
let include_usage = thread_store
|
||||
.get::<ModelInfo>()
|
||||
@@ -183,7 +182,7 @@ where
|
||||
capped_skill_metadata_budget(/*context_window*/ None),
|
||||
);
|
||||
if let Some(message) = rendered.warning_message {
|
||||
self.emit_warning(thread_store.level_id(), message);
|
||||
self.emit_warning(thread_store.level_id(), /*turn_id*/ None, message);
|
||||
}
|
||||
rendered
|
||||
.fragment
|
||||
@@ -247,7 +246,7 @@ where
|
||||
.get_or_init(EmittedCatalogBudgetWarnings::default)
|
||||
.insert(&message)
|
||||
{
|
||||
self.emit_warning(input.turn_id, message);
|
||||
self.emit_warning(input.thread_store.level_id(), Some(input.turn_id), message);
|
||||
}
|
||||
let executor_body = rendered.fragment.map(|fragment| fragment.body());
|
||||
let mut sections = vec![executor_skills_world_state_section(
|
||||
@@ -364,8 +363,8 @@ where
|
||||
.map(|executor_skills| executor_skills.0.clone())
|
||||
.unwrap_or_default();
|
||||
catalog.extend(self.list_skills(query, &thread_state).await);
|
||||
for warning in &catalog.warnings {
|
||||
self.emit_warning(&input.turn_id, warning.clone());
|
||||
for warning in bounded_warnings(&catalog.warnings) {
|
||||
self.emit_warning(thread_store.level_id(), Some(&input.turn_id), warning);
|
||||
}
|
||||
|
||||
let selected_entries = collect_explicit_skill_mentions(&input.user_input, &catalog);
|
||||
@@ -407,7 +406,7 @@ where
|
||||
metadata_budget,
|
||||
);
|
||||
if let Some(message) = rendered.warning_message {
|
||||
self.emit_warning(&input.turn_id, message);
|
||||
self.emit_warning(thread_store.level_id(), Some(&input.turn_id), message);
|
||||
}
|
||||
if let Some(fragment) = rendered.fragment {
|
||||
fragments.push(Box::new(fragment));
|
||||
@@ -435,7 +434,11 @@ where
|
||||
"Skill `{}` exceeded the main prompt context limit and was truncated.",
|
||||
entry.name
|
||||
);
|
||||
self.emit_warning(&input.turn_id, warning.clone());
|
||||
self.emit_warning(
|
||||
thread_store.level_id(),
|
||||
Some(&input.turn_id),
|
||||
warning.clone(),
|
||||
);
|
||||
warnings.push(warning);
|
||||
}
|
||||
let fragment = SkillInstructions {
|
||||
@@ -455,7 +458,11 @@ where
|
||||
}
|
||||
Err(message) => {
|
||||
let warning = format!("Failed to load skill `{}`: {message}", entry.name);
|
||||
self.emit_warning(&input.turn_id, warning.clone());
|
||||
self.emit_warning(
|
||||
thread_store.level_id(),
|
||||
Some(&input.turn_id),
|
||||
warning.clone(),
|
||||
);
|
||||
warnings.push(warning);
|
||||
}
|
||||
}
|
||||
@@ -542,10 +549,11 @@ impl<C> SkillsExtension<C> {
|
||||
.map_err(|err| err.message)
|
||||
}
|
||||
|
||||
fn emit_warning(&self, turn_id: &str, message: String) {
|
||||
self.event_sink.emit(Event {
|
||||
id: turn_id.to_string(),
|
||||
msg: EventMsg::Warning(WarningEvent { message }),
|
||||
fn emit_warning(&self, thread_id: &str, turn_id: Option<&str>, message: String) {
|
||||
self.event_sink.emit_warning(ExtensionWarning {
|
||||
thread_id: thread_id.to_string(),
|
||||
turn_id: turn_id.map(str::to_string),
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ mod shadow_selection_experiment;
|
||||
mod sources;
|
||||
mod state;
|
||||
mod tools;
|
||||
mod warnings;
|
||||
mod world_state;
|
||||
|
||||
pub use config::SkillsExtensionConfig;
|
||||
|
||||
@@ -9,7 +9,7 @@ use serde::Serialize;
|
||||
|
||||
use crate::catalog::SkillCatalogEntry;
|
||||
use crate::render::truncate_catalog_skill_description;
|
||||
use crate::render::truncate_utf8_to_bytes;
|
||||
use crate::warnings::bounded_warnings;
|
||||
|
||||
use super::MAX_HANDLE_BYTES;
|
||||
use super::SkillToolAuthority;
|
||||
@@ -21,8 +21,6 @@ use super::skill_function_tool;
|
||||
use super::skill_tool_name;
|
||||
|
||||
const TOOL_NAME: &str = "list";
|
||||
const MAX_WARNINGS: usize = 4;
|
||||
const MAX_WARNING_BYTES: usize = 256;
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -76,7 +74,7 @@ impl ToolExecutor<ToolCall> for ListTool {
|
||||
.filter(|entry| entry.enabled && entry.authority == authority)
|
||||
.filter_map(listed_skill)
|
||||
.collect(),
|
||||
warnings: bounded_warnings(catalog.warnings),
|
||||
warnings: bounded_warnings(&catalog.warnings),
|
||||
};
|
||||
|
||||
external_json_output(&response)
|
||||
@@ -100,14 +98,3 @@ fn listed_skill(entry: SkillCatalogEntry) -> Option<ListedSkill> {
|
||||
main_resource: entry.main_prompt.as_str().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn bounded_warnings(warnings: Vec<String>) -> Vec<String> {
|
||||
warnings
|
||||
.into_iter()
|
||||
.take(MAX_WARNINGS)
|
||||
.map(|warning| {
|
||||
let (warning, _) = truncate_utf8_to_bytes(&warning, MAX_WARNING_BYTES);
|
||||
warning
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
use crate::render::truncate_utf8_to_bytes;
|
||||
|
||||
const MAX_WARNINGS: usize = 4;
|
||||
const MAX_WARNING_BYTES: usize = 256;
|
||||
|
||||
pub(crate) fn bounded_warnings(warnings: &[String]) -> Vec<String> {
|
||||
warnings
|
||||
.iter()
|
||||
.take(MAX_WARNINGS)
|
||||
.map(|warning| truncate_utf8_to_bytes(warning, MAX_WARNING_BYTES).0)
|
||||
.collect()
|
||||
}
|
||||
Reference in new issue
Block a user