Inject SKILL.md when it's explicitly mentioned. (#7763)

1. Skills load once in core at session start; the cached outcome is
reused across core and surfaced to TUI via SessionConfigured.
2. TUI detects explicit skill selections, and core injects the matching
SKILL.md content into the turn when a selected skill is present.
This commit is contained in:
xl-openai
2025-12-10 13:59:17 -08:00
committed by GitHub
parent eb2e5458cc
commit b36ecb6c32
21 changed files with 584 additions and 88 deletions

View File

@@ -300,36 +300,37 @@ impl From<Vec<UserInput>> for ResponseInputItem {
role: "user".to_string(),
content: items
.into_iter()
.map(|c| match c {
UserInput::Text { text } => ContentItem::InputText { text },
UserInput::Image { image_url } => ContentItem::InputImage { image_url },
.filter_map(|c| match c {
UserInput::Text { text } => Some(ContentItem::InputText { text }),
UserInput::Image { image_url } => Some(ContentItem::InputImage { image_url }),
UserInput::LocalImage { path } => match load_and_resize_to_fit(&path) {
Ok(image) => ContentItem::InputImage {
Ok(image) => Some(ContentItem::InputImage {
image_url: image.into_data_url(),
},
}),
Err(err) => {
if matches!(&err, ImageProcessingError::Read { .. }) {
local_image_error_placeholder(&path, &err)
Some(local_image_error_placeholder(&path, &err))
} else if err.is_invalid_image() {
invalid_image_error_placeholder(&path, &err)
Some(invalid_image_error_placeholder(&path, &err))
} else {
let Some(mime_guess) = mime_guess::from_path(&path).first() else {
return local_image_error_placeholder(
return Some(local_image_error_placeholder(
&path,
"unsupported MIME type (unknown)",
);
));
};
let mime = mime_guess.essence_str().to_owned();
if !mime.starts_with("image/") {
return local_image_error_placeholder(
return Some(local_image_error_placeholder(
&path,
format!("unsupported MIME type `{mime}`"),
);
));
}
unsupported_image_error_placeholder(&path, &mime)
Some(unsupported_image_error_placeholder(&path, &mime))
}
}
},
UserInput::Skill { .. } => None, // Skill bodies are injected later in core
})
.collect::<Vec<ContentItem>>(),
}

View File

@@ -1624,6 +1624,25 @@ pub struct ListCustomPromptsResponseEvent {
pub custom_prompts: Vec<CustomPrompt>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct SkillInfo {
pub name: String,
pub description: String,
pub path: PathBuf,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct SkillErrorInfo {
pub path: PathBuf,
pub message: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, Default)]
pub struct SkillLoadOutcomeInfo {
pub skills: Vec<SkillInfo>,
pub errors: Vec<SkillErrorInfo>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct SessionConfiguredEvent {
/// Name left as session_id instead of conversation_id for backwards compatibility.
@@ -1659,6 +1678,9 @@ pub struct SessionConfiguredEvent {
#[serde(skip_serializing_if = "Option::is_none")]
pub initial_messages: Option<Vec<EventMsg>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub skill_load_outcome: Option<SkillLoadOutcomeInfo>,
pub rollout_path: PathBuf,
}
@@ -1786,6 +1808,7 @@ mod tests {
history_log_id: 0,
history_entry_count: 0,
initial_messages: None,
skill_load_outcome: None,
rollout_path: rollout_file.path().to_path_buf(),
}),
};

View File

@@ -21,4 +21,10 @@ pub enum UserInput {
LocalImage {
path: std::path::PathBuf,
},
/// Skill selected by the user (name + path to SKILL.md).
Skill {
name: String,
path: std::path::PathBuf,
},
}