use std::fmt::Write as _;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::error::{AgentError, ErrorKind};
use crate::tools::{Tool, ToolContext, ToolEffect};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Skill {
pub name: String,
pub description: String,
pub body: String,
}
impl Skill {
#[must_use]
pub fn new(
name: impl Into<String>,
description: impl Into<String>,
body: impl Into<String>,
) -> Self {
Self {
name: name.into(),
description: description.into(),
body: body.into(),
}
}
pub fn parse(document: &str) -> Result<Self, AgentError> {
let document = document.trim_start_matches('\u{feff}');
let mut lines = document.lines();
if lines.next().map(str::trim) != Some("---") {
return Err(AgentError::new(
ErrorKind::Config,
"skill document must start with a `---` front matter line",
));
}
let mut name = None;
let mut description = None;
let mut closed = false;
for line in lines.by_ref() {
if line.trim() == "---" {
closed = true;
break;
}
if let Some((key, value)) = line.split_once(':') {
let value = unquote(value.trim());
match key.trim() {
"name" => name = Some(value.to_owned()),
"description" => description = Some(value.to_owned()),
_ => {}
}
}
}
if !closed {
return Err(AgentError::new(
ErrorKind::Config,
"skill front matter is not closed with `---`",
));
}
let body = lines.collect::<Vec<_>>().join("\n").trim().to_owned();
let name = name.filter(|name| !name.is_empty()).ok_or_else(|| {
AgentError::new(ErrorKind::Config, "skill front matter needs a `name`")
})?;
let description = description
.filter(|description| !description.is_empty())
.ok_or_else(|| {
AgentError::new(
ErrorKind::Config,
format!("skill {name:?} needs a `description`"),
)
})?;
Ok(Self {
name,
description,
body,
})
}
}
fn unquote(value: &str) -> &str {
for quote in ['"', '\''] {
if let Some(inner) = value
.strip_prefix(quote)
.and_then(|rest| rest.strip_suffix(quote))
{
return inner;
}
}
value
}
#[must_use]
pub fn render_index(skills: &[Skill]) -> String {
let mut out = String::from(
"## Skills\nBefore you do a task that a skill covers, read it with the `load_skill` tool.\n",
);
for skill in skills {
let _ = writeln!(out, "- {}: {}", skill.name, skill.description);
}
out
}
#[derive(Debug, Clone)]
pub struct SkillTool {
skills: Arc<[Skill]>,
}
impl SkillTool {
#[must_use]
pub const fn new(skills: Arc<[Skill]>) -> Self {
Self { skills }
}
}
impl SkillTool {
fn lookup(&self, name: &str) -> Result<serde_json::Value, AgentError> {
self.skills
.iter()
.find(|skill| skill.name == name)
.map(|skill| serde_json::json!({"name": skill.name, "instructions": skill.body}))
.ok_or_else(|| {
let known: Vec<&str> = self
.skills
.iter()
.map(|skill| skill.name.as_str())
.collect();
AgentError::new(
ErrorKind::Tool,
format!("no skill {name:?}; skills: {known:?}"),
)
})
}
}
impl Tool for SkillTool {
fn name(&self) -> &'static str {
"load_skill"
}
fn description(&self) -> &'static str {
"Read the full instructions of one skill from the Skills list."
}
fn input_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {"name": {"type": "string", "description": "Skill name."}},
"required": ["name"]
})
}
fn effect(&self) -> ToolEffect {
ToolEffect::ReadOnly
}
fn execute<'a>(
&'a self,
input: serde_json::Value,
_ctx: &'a ToolContext,
) -> Pin<Box<dyn Future<Output = Result<serde_json::Value, AgentError>> + Send + 'a>> {
let result = input
.get("name")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| AgentError::new(ErrorKind::Tool, "give the skill `name`"))
.and_then(|name| self.lookup(name));
Box::pin(std::future::ready(result))
}
}
#[cfg(test)]
mod tests;