mirror of
https://github.com/openai/codex.git
synced 2026-11-06 00:30:29 +00:00
## 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
101 lines
3.0 KiB
Rust
101 lines
3.0 KiB
Rust
use codex_extension_api::ToolCall;
|
|
use codex_extension_api::ToolExecutor;
|
|
use codex_extension_api::ToolExecutorFuture;
|
|
use codex_extension_api::ToolName;
|
|
use codex_extension_api::ToolSpec;
|
|
use schemars::JsonSchema;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
|
|
use crate::catalog::SkillCatalogEntry;
|
|
use crate::render::truncate_catalog_skill_description;
|
|
use crate::warnings::bounded_warnings;
|
|
|
|
use super::MAX_HANDLE_BYTES;
|
|
use super::SkillToolAuthority;
|
|
use super::SkillToolContext;
|
|
use super::external_json_output;
|
|
use super::is_bounded_handle;
|
|
use super::parse_args;
|
|
use super::skill_function_tool;
|
|
use super::skill_tool_name;
|
|
|
|
const TOOL_NAME: &str = "list";
|
|
|
|
#[derive(Deserialize, JsonSchema)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct ListArgs {
|
|
authority: SkillToolAuthority,
|
|
}
|
|
|
|
#[derive(Debug, Eq, JsonSchema, PartialEq, Serialize)]
|
|
#[schemars(deny_unknown_fields)]
|
|
struct ListedSkill {
|
|
authority: SkillToolAuthority,
|
|
package: String,
|
|
name: String,
|
|
description: String,
|
|
main_resource: String,
|
|
}
|
|
|
|
#[derive(Debug, Eq, JsonSchema, PartialEq, Serialize)]
|
|
#[schemars(deny_unknown_fields)]
|
|
struct ListResponse {
|
|
skills: Vec<ListedSkill>,
|
|
warnings: Vec<String>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub(super) struct ListTool {
|
|
pub(super) context: SkillToolContext,
|
|
}
|
|
|
|
impl ToolExecutor<ToolCall> for ListTool {
|
|
fn tool_name(&self) -> ToolName {
|
|
skill_tool_name(TOOL_NAME)
|
|
}
|
|
|
|
fn spec(&self) -> ToolSpec {
|
|
skill_function_tool::<ListArgs, ListResponse>(
|
|
TOOL_NAME,
|
|
"List enabled skills owned by the requested authority. Only orchestrator-owned skills are currently supported. Returns the opaque package and main-resource handles required by skills.read.",
|
|
)
|
|
}
|
|
|
|
fn handle(&self, call: ToolCall) -> ToolExecutorFuture<'_> {
|
|
Box::pin(async move {
|
|
let args: ListArgs = parse_args(&call)?;
|
|
let authority = args.authority.into_authority();
|
|
let catalog = self.context.catalog(&call.turn_id, args.authority).await;
|
|
let response = ListResponse {
|
|
skills: catalog
|
|
.entries
|
|
.into_iter()
|
|
.filter(|entry| entry.enabled && entry.authority == authority)
|
|
.filter_map(listed_skill)
|
|
.collect(),
|
|
warnings: bounded_warnings(&catalog.warnings),
|
|
};
|
|
|
|
external_json_output(&response)
|
|
})
|
|
}
|
|
}
|
|
|
|
fn listed_skill(entry: SkillCatalogEntry) -> Option<ListedSkill> {
|
|
let authority = SkillToolAuthority::from_authority(&entry.authority)?;
|
|
if !is_bounded_handle(&entry.id.0, MAX_HANDLE_BYTES)
|
|
|| !is_bounded_handle(entry.main_prompt.as_str(), MAX_HANDLE_BYTES)
|
|
{
|
|
return None;
|
|
}
|
|
|
|
Some(ListedSkill {
|
|
authority,
|
|
package: entry.id.0,
|
|
name: entry.name,
|
|
description: truncate_catalog_skill_description(&entry.description).into_owned(),
|
|
main_resource: entry.main_prompt.as_str().to_string(),
|
|
})
|
|
}
|