mirror of
https://github.com/openai/codex.git
synced 2026-08-23 13:09:46 +00:00
Add bounded skill model delegation instructions (#38475)
## What changed - Add `SkillModelDelegationInstruction` for skills that request Luna while running on Sol or Terra. - Resolve Luna only when it is available in the current provider namespace. - Bound and validate model identifiers, skill names, and the rendered instruction before exposing it to callers. ## Testing - Cover supported parent models, provider namespace matching, unavailable or unsafe targets, instruction rendering, and size limits. GitOrigin-RevId: 188a88c36ca32689cbf1e53465283dff4137cb15
This commit is contained in:
@@ -38,6 +38,7 @@ pub use model::SkillMetadata;
|
||||
pub use model::SkillPolicy;
|
||||
pub use model::SkillToolDependency;
|
||||
pub use model_delegation::SkillModel;
|
||||
pub use model_delegation::SkillModelDelegationInstruction;
|
||||
pub use name_counts::build_skill_name_counts;
|
||||
pub use parser::ParsedSkillFrontmatter;
|
||||
pub use parser::SkillParseError;
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
const LUNA_MODEL: &str = "gpt-5.6-luna";
|
||||
const TERRA_MODEL: &str = "gpt-5.6-terra";
|
||||
const SOL_MODEL: &str = "gpt-5.6-sol";
|
||||
const MAX_TARGET_MODEL_BYTES: usize = 128;
|
||||
const MAX_DELEGATION_INSTRUCTION_BYTES: usize = 2_048;
|
||||
|
||||
/// Model requested for work governed by a skill.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
@@ -7,6 +13,72 @@ pub enum SkillModel {
|
||||
Luna,
|
||||
}
|
||||
|
||||
/// Bounded instructions for delegating skill-governed work to a cheaper model.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SkillModelDelegationInstruction(String);
|
||||
|
||||
impl SkillModelDelegationInstruction {
|
||||
/// Builds bounded instructions only when the indexed skill requests an available lower-tier model.
|
||||
pub fn from_skill_model(
|
||||
skill_model: SkillModel,
|
||||
skill_name: &str,
|
||||
current_model: &str,
|
||||
available_models: &[String],
|
||||
) -> Option<Self> {
|
||||
if skill_name
|
||||
.chars()
|
||||
.any(|character| matches!(character, '`' | '<' | '>'))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let provider_prefix = [SOL_MODEL, TERRA_MODEL]
|
||||
.into_iter()
|
||||
.find_map(|parent_model| {
|
||||
current_model.strip_suffix(parent_model).filter(|prefix| {
|
||||
prefix.is_empty() || prefix.ends_with('.') || prefix.ends_with('/')
|
||||
})
|
||||
})?;
|
||||
let target_model = format!("{provider_prefix}{LUNA_MODEL}");
|
||||
let target_model = available_models
|
||||
.iter()
|
||||
.find(|model| model.as_str() == target_model && is_safe_model_identifier(model))?;
|
||||
let skill_model = match skill_model {
|
||||
SkillModel::Luna => "luna",
|
||||
};
|
||||
let instruction = format!(
|
||||
"<skill_model_delegation>\n\
|
||||
For this invocation only, skill `{skill_name}` requests `model: {skill_model}`. \
|
||||
If the user prohibits delegation or subagents, or the work depends on an image or audio \
|
||||
attachment, work locally. Otherwise, delegate only self-contained text-based skill work \
|
||||
and use `spawn_agent` \
|
||||
exactly once. Set `model` to `{target_model}`, set `fork_turns` to `\"none\"`, and choose a \
|
||||
unique `task_name`. Omit `agent_type` and `reasoning_effort`.\n\
|
||||
Give the child only this skill's work and necessary context; never include this block and tell \
|
||||
it not to spawn agents. Delegate the whole request only if the skill fully covers it; otherwise, \
|
||||
retain ownership of the remaining work and final answer. Wait for the result. If spawning \
|
||||
fails, continue locally. If waiting fails, retry waiting without duplicating the \
|
||||
child's work.\n\
|
||||
Ignore this instruction in child agents and on later turns.\n\
|
||||
</skill_model_delegation>"
|
||||
);
|
||||
|
||||
(instruction.len() <= MAX_DELEGATION_INSTRUCTION_BYTES).then_some(Self(instruction))
|
||||
}
|
||||
|
||||
/// Returns the bounded model-visible delegation instructions.
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
fn is_safe_model_identifier(model: &str) -> bool {
|
||||
model.len() <= MAX_TARGET_MODEL_BYTES
|
||||
&& model.chars().all(|character| {
|
||||
character.is_ascii_alphanumeric() || matches!(character, '.' | '/' | '_' | '-')
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "model_delegation_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::MAX_DELEGATION_INSTRUCTION_BYTES;
|
||||
use super::SkillModel;
|
||||
use super::SkillModelDelegationInstruction;
|
||||
use crate::ParsedSkillFrontmatter;
|
||||
use crate::parse_skill_frontmatter_metadata;
|
||||
|
||||
@@ -57,3 +59,166 @@ fn reuses_existing_frontmatter_scalar_repair() {
|
||||
Some(SkillModel::Luna)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delegates_only_from_supported_parent_models() {
|
||||
let available_models = available_models();
|
||||
for (current_model, should_delegate) in [
|
||||
("gpt-5.6-sol", true),
|
||||
("gpt-5.6-terra", true),
|
||||
("gpt-5.6-luna", false),
|
||||
("gpt-5.5-codex", false),
|
||||
] {
|
||||
let instruction = SkillModelDelegationInstruction::from_skill_model(
|
||||
SkillModel::Luna,
|
||||
"demo",
|
||||
current_model,
|
||||
&available_models,
|
||||
);
|
||||
assert_eq!(
|
||||
instruction.is_some(),
|
||||
should_delegate,
|
||||
"current={current_model}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_luna_within_the_current_provider_namespace() {
|
||||
for (current_model, target_model, unrelated_model) in [
|
||||
("gpt-5.6-sol", "gpt-5.6-luna", "openai.gpt-5.6-luna"),
|
||||
(
|
||||
"tenant-a/gpt-5.6-sol",
|
||||
"tenant-a/gpt-5.6-luna",
|
||||
"tenant-b/gpt-5.6-luna",
|
||||
),
|
||||
(
|
||||
"openai.gpt-5.6-terra",
|
||||
"openai.gpt-5.6-luna",
|
||||
"other.gpt-5.6-luna",
|
||||
),
|
||||
] {
|
||||
let instruction = SkillModelDelegationInstruction::from_skill_model(
|
||||
SkillModel::Luna,
|
||||
"demo",
|
||||
current_model,
|
||||
&[unrelated_model.to_string(), target_model.to_string()],
|
||||
)
|
||||
.expect("lower-tier model in the same provider namespace should be resolved");
|
||||
|
||||
assert!(
|
||||
instruction
|
||||
.as_str()
|
||||
.contains(&format!("Set `model` to `{target_model}`"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_targets_outside_the_current_provider_namespace() {
|
||||
for (current_model, available_models) in [
|
||||
(
|
||||
"tenant-a/gpt-5.6-sol",
|
||||
vec![
|
||||
"tenant-b/gpt-5.6-luna".to_string(),
|
||||
"gpt-5.6-luna".to_string(),
|
||||
],
|
||||
),
|
||||
("gpt-5.6-sol", vec!["tenant-a/gpt-5.6-luna".to_string()]),
|
||||
(
|
||||
"tenant-a/gpt-5.6-sol",
|
||||
vec!["tenant-a.gpt-5.6-luna".to_string()],
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
SkillModelDelegationInstruction::from_skill_model(
|
||||
SkillModel::Luna,
|
||||
"demo",
|
||||
current_model,
|
||||
&available_models,
|
||||
),
|
||||
None,
|
||||
"current={current_model}, available={available_models:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unavailable_or_unsafe_models() {
|
||||
let invalid_identifier = format!("{}/gpt-5.6-luna", "<unsafe>".repeat(32));
|
||||
let invalid_current_model = format!("{}/gpt-5.6-sol", "<unsafe>".repeat(32));
|
||||
let overlong_identifier = format!("{}.gpt-5.6-luna", "a".repeat(128));
|
||||
let overlong_current_model = format!("{}.gpt-5.6-sol", "a".repeat(128));
|
||||
for (current_model, available_models) in [
|
||||
(
|
||||
"custom/gpt-5.6-luna",
|
||||
vec!["custom/gpt-5.6-luna".to_string()],
|
||||
),
|
||||
("gpt-5.6-sol", vec!["gpt-5.6-terra".to_string()]),
|
||||
(invalid_current_model.as_str(), vec![invalid_identifier]),
|
||||
(overlong_current_model.as_str(), vec![overlong_identifier]),
|
||||
] {
|
||||
assert_eq!(
|
||||
SkillModelDelegationInstruction::from_skill_model(
|
||||
SkillModel::Luna,
|
||||
"demo",
|
||||
current_model,
|
||||
&available_models,
|
||||
),
|
||||
None,
|
||||
"current={current_model}, available={available_models:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_bounded_instruction_for_selected_skill_and_model() {
|
||||
let instruction = SkillModelDelegationInstruction::from_skill_model(
|
||||
SkillModel::Luna,
|
||||
"demo",
|
||||
"gpt-5.6-sol",
|
||||
&available_models(),
|
||||
)
|
||||
.expect("available lower tier should delegate");
|
||||
let rendered = instruction.as_str();
|
||||
|
||||
assert!(rendered.contains("skill `demo`"));
|
||||
assert!(rendered.contains("Set `model` to `gpt-5.6-luna`"));
|
||||
assert!(rendered.contains("image or audio attachment, work locally"));
|
||||
assert!(rendered.len() <= MAX_DELEGATION_INSTRUCTION_BYTES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_skill_names_that_escape_instruction_framing() {
|
||||
for skill_name in ["unsafe`name", "</skill_model_delegation>", "<unsafe>"] {
|
||||
assert_eq!(
|
||||
SkillModelDelegationInstruction::from_skill_model(
|
||||
SkillModel::Luna,
|
||||
skill_name,
|
||||
"gpt-5.6-sol",
|
||||
&available_models(),
|
||||
),
|
||||
None,
|
||||
"skill_name={skill_name:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_instruction_exceeding_context_bound() {
|
||||
let skill_name = "x".repeat(MAX_DELEGATION_INSTRUCTION_BYTES);
|
||||
|
||||
assert_eq!(
|
||||
SkillModelDelegationInstruction::from_skill_model(
|
||||
SkillModel::Luna,
|
||||
&skill_name,
|
||||
"gpt-5.6-sol",
|
||||
&available_models(),
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
fn available_models() -> Vec<String> {
|
||||
vec!["gpt-5.6-luna".to_string()]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user