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:
felixxia-oai
2026-08-14 01:36:54 +00:00
committed by copyberry
parent 5c6f498b0e
commit 5620bab61c
3 changed files with 238 additions and 0 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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()]
}