fix: reduce summon frontmatter warning noise (#8363)
Signed-off-by: Douwe Osinga <douwe@squareup.com> Co-authored-by: txhno <198242577+txhno@users.noreply.github.com> Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
@@ -19,7 +19,6 @@ use crate::agents::mcp_client::McpClientTrait;
|
|||||||
use crate::session::Session;
|
use crate::session::Session;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tracing::warn;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Source {
|
pub struct Source {
|
||||||
@@ -61,23 +60,19 @@ impl Source {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_frontmatter<T: for<'de> Deserialize<'de>>(content: &str) -> Option<(T, String)> {
|
pub fn parse_frontmatter<T: for<'de> Deserialize<'de>>(
|
||||||
|
content: &str,
|
||||||
|
) -> Result<Option<(T, String)>, serde_yaml::Error> {
|
||||||
let parts: Vec<&str> = content.split("---").collect();
|
let parts: Vec<&str> = content.split("---").collect();
|
||||||
if parts.len() < 3 {
|
if parts.len() < 3 {
|
||||||
return None;
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let yaml_content = parts[1].trim();
|
let yaml_content = parts[1].trim();
|
||||||
let metadata: T = match serde_yaml::from_str(yaml_content) {
|
let metadata: T = serde_yaml::from_str(yaml_content)?;
|
||||||
Ok(m) => m,
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Failed to parse frontmatter: {}", e);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let body = parts[2..].join("---").trim().to_string();
|
let body = parts[2..].join("---").trim().to_string();
|
||||||
Some((metadata, body))
|
Ok(Some((metadata, body)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub use ext_manager::MANAGE_EXTENSIONS_TOOL_NAME_COMPLETE;
|
pub use ext_manager::MANAGE_EXTENSIONS_TOOL_NAME_COMPLETE;
|
||||||
|
|||||||
@@ -25,7 +25,14 @@ struct SkillMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_skill_content(content: &str, path: PathBuf) -> Option<Source> {
|
fn parse_skill_content(content: &str, path: PathBuf) -> Option<Source> {
|
||||||
let (metadata, body): (SkillMetadata, String) = parse_frontmatter(content)?;
|
let (metadata, body): (SkillMetadata, String) = match parse_frontmatter(content) {
|
||||||
|
Ok(Some(parsed)) => parsed,
|
||||||
|
Ok(None) => return None,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to parse skill frontmatter: {}", e);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if metadata.name.contains('/') {
|
if metadata.name.contains('/') {
|
||||||
warn!("Skill name '{}' contains '/', skipping", metadata.name);
|
warn!("Skill name '{}' contains '/', skipping", metadata.name);
|
||||||
|
|||||||
@@ -95,8 +95,20 @@ struct AgentMetadata {
|
|||||||
model: Option<String>,
|
model: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_agent_content(content: &str, path: PathBuf) -> Option<Source> {
|
fn parse_agent_content(content: &str, path: &Path) -> Option<Source> {
|
||||||
let (metadata, body): (AgentMetadata, String) = parse_frontmatter(content)?;
|
let (metadata, body): (AgentMetadata, String) = match parse_frontmatter(content) {
|
||||||
|
Ok(Some(parsed)) => parsed,
|
||||||
|
Ok(None) => return None,
|
||||||
|
Err(e) => {
|
||||||
|
// Missing fields means this file has valid YAML but isn't an agent — skip silently.
|
||||||
|
// Only warn on actual YAML syntax errors.
|
||||||
|
if e.to_string().contains("missing field") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
warn!("Failed to parse agent file {}: {}", path.display(), e);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let description = metadata.description.unwrap_or_else(|| {
|
let description = metadata.description.unwrap_or_else(|| {
|
||||||
let model_info = metadata
|
let model_info = metadata
|
||||||
@@ -111,7 +123,7 @@ fn parse_agent_content(content: &str, path: PathBuf) -> Option<Source> {
|
|||||||
name: metadata.name,
|
name: metadata.name,
|
||||||
kind: SourceKind::Agent,
|
kind: SourceKind::Agent,
|
||||||
description,
|
description,
|
||||||
path,
|
path: path.to_path_buf(),
|
||||||
content: body,
|
content: body,
|
||||||
supporting_files: Vec::new(),
|
supporting_files: Vec::new(),
|
||||||
})
|
})
|
||||||
@@ -197,7 +209,7 @@ fn scan_agents_from_dir(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(source) = parse_agent_content(&content, path) {
|
if let Some(source) = parse_agent_content(&content, &path) {
|
||||||
if !seen.contains(&source.name) {
|
if !seen.contains(&source.name) {
|
||||||
seen.insert(source.name.clone());
|
seen.insert(source.name.clone());
|
||||||
sources.push(source);
|
sources.push(source);
|
||||||
@@ -1182,8 +1194,9 @@ impl SummonClient {
|
|||||||
.map_err(|e| format!("Failed to read agent file: {}", e))?
|
.map_err(|e| format!("Failed to read agent file: {}", e))?
|
||||||
};
|
};
|
||||||
|
|
||||||
let (metadata, _): (AgentMetadata, String) =
|
let (metadata, _): (AgentMetadata, String) = parse_frontmatter(&agent_content)
|
||||||
parse_frontmatter(&agent_content).ok_or("Failed to parse agent frontmatter")?;
|
.map_err(|e| format!("Failed to parse agent frontmatter: {}", e))?
|
||||||
|
.ok_or("No frontmatter found in agent file")?;
|
||||||
|
|
||||||
let model = metadata.model;
|
let model = metadata.model;
|
||||||
|
|
||||||
@@ -1647,6 +1660,7 @@ impl McpClientTrait for SummonClient {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
@@ -1666,11 +1680,46 @@ name: reviewer
|
|||||||
model: sonnet
|
model: sonnet
|
||||||
---
|
---
|
||||||
You review code."#;
|
You review code."#;
|
||||||
let source = parse_agent_content(agent, PathBuf::new()).unwrap();
|
let source = parse_agent_content(agent, Path::new("")).unwrap();
|
||||||
assert_eq!(source.name, "reviewer");
|
assert_eq!(source.name, "reviewer");
|
||||||
assert!(source.description.contains("sonnet"));
|
assert!(source.description.contains("sonnet"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_agent_scan_skips_non_agent_markdown() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let agents_dir = temp_dir.path().join("agents");
|
||||||
|
fs::create_dir_all(&agents_dir).unwrap();
|
||||||
|
fs::write(
|
||||||
|
agents_dir.join("README.md"),
|
||||||
|
"---\ntitle: Notes\n---\nThis is not an agent.",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
fs::write(
|
||||||
|
agents_dir.join("notes.md"),
|
||||||
|
"---\nauthor: someone\ntags: [docs]\n---\nJust documentation.",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
fs::write(
|
||||||
|
agents_dir.join("reviewer.md"),
|
||||||
|
"---\nname: reviewer\nmodel: sonnet\n---\nYou review code.",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
fs::write(agents_dir.join("plain.md"), "No frontmatter at all.").unwrap();
|
||||||
|
fs::write(
|
||||||
|
agents_dir.join("broken.md"),
|
||||||
|
"---\nname: [unterminated\n---\nBroken YAML.",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut sources = Vec::new();
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
scan_agents_from_dir(&agents_dir, &mut sources, &mut seen);
|
||||||
|
|
||||||
|
assert_eq!(sources.len(), 1);
|
||||||
|
assert_eq!(sources[0].name, "reviewer");
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_discover_recipes_and_agents() {
|
async fn test_discover_recipes_and_agents() {
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user