PicoBot/src/tools/get_skill.rs
xiaoxixi ac201a3949 feat: durable agent orchestration with run persistence, inbox continuation, and signal/steer
- AgentCatalog/definitions with strict Markdown frontmatter, delegation graph,
  fail-closed tool scoping, and signal contracts
- structured cancellation (AgentError::Cancelled/TimedOut) across provider
  streams, tool batches, and sleep; /stop drives the same terminal state
- schema v6 run/group/inbox persistence with execution-ID conditional
  transitions and completion-slot reservations
- ExecutionGate separating run quota from provider/tool step permits
- background completion inbox with hidden-trigger continuation turns,
  fairness scheduling, lease release, dead-lettering, and activation recovery
- typed TurnMailbox with two-phase steer admission and atomic consumption at
  turn commit; /stop releases admitted steer events back to pending
- emit_signal tool with contract-enforced rate/dedupe/severity/size limits
- WS run/event projection (GetAgentRuns, AgentRunUpdated, AgentEventUpdated),
  /api/agent-runs* management endpoints, /api/tasks union, WebUI run tree
  and signal cards
- ChannelContext.durable_private persisted for continuation delivery reuse

Version 1.7.0
2026-08-11 11:51:20 +08:00

244 lines
7.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use std::collections::HashSet;
use std::sync::Arc;
use async_trait::async_trait;
use serde_json::json;
use crate::skills::{Skill, SkillsLoader};
use crate::tools::traits::{Tool, ToolResult};
pub struct GetSkillTool {
skills_loader: Arc<SkillsLoader>,
allowed: Option<HashSet<String>>,
}
impl GetSkillTool {
pub fn new(skills_loader: Arc<SkillsLoader>) -> Self {
Self {
skills_loader,
allowed: None,
}
}
pub fn scoped(skills_loader: Arc<SkillsLoader>, allowed: &[String]) -> Self {
Self {
skills_loader,
allowed: Some(allowed.iter().cloned().collect()),
}
}
fn format_skill(&self, skill: &Skill) -> String {
let mut result = format!("# Skill: {}\n\n{}", skill.name, skill.description);
if let Some(path) = &skill.path {
result.push_str(&format!(
"\n\n**Skill Root Directory:** `{}`\n\nAll files and references in this skill are relative to this directory.",
path.to_string_lossy()
));
}
result.push_str(&format!("\n\n---\n\n{}", skill.content));
result
}
}
#[async_trait]
impl Tool for GetSkillTool {
fn delegation_policy(&self) -> crate::tools::DelegationPolicy {
crate::tools::DelegationPolicy::RuntimeInjected
}
fn name(&self) -> &str {
"get_skill"
}
fn description(&self) -> &str {
"Get complete content and guidance for a specified skill. Use this when you need detailed instructions for a specific type of task."
}
fn parameters_schema(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["get", "list"],
"description": "操作类型: get 获取指定 skill 完整内容, list 列出所有可用 skill"
},
"skill_name": {
"type": "string",
"description": "Name of the skill to retrieve仅在 action 为 get 时必填"
}
},
"required": []
})
}
fn read_only(&self) -> bool {
true
}
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
let action = args.get("action").and_then(|v| v.as_str()).unwrap_or("get");
match action {
"list" => self.list_skills_full(),
_ => self.get_skill_by_name(&args),
}
}
}
impl GetSkillTool {
fn get_skill_by_name(&self, args: &serde_json::Value) -> anyhow::Result<ToolResult> {
let skill_name = match args.get("skill_name").and_then(|v| v.as_str()) {
Some(name) => name,
None => {
return Ok(ToolResult {
success: false,
output: String::new(),
error: Some("Missing required parameter: skill_name".to_string()),
});
}
};
if self
.allowed
.as_ref()
.is_some_and(|allowed| !allowed.contains(skill_name))
{
return Ok(ToolResult {
success: false,
output: String::new(),
error: Some(format!(
"Skill '{skill_name}' is not allowed for this Agent"
)),
});
}
match self.skills_loader.get_skill(skill_name) {
Some(skill) => {
let formatted = self.format_skill(&skill);
Ok(ToolResult {
success: true,
output: formatted,
error: None,
})
}
None => {
let available = self.skills_loader.list_skills();
let available_str = if available.is_empty() {
"No skills available".to_string()
} else {
available
.iter()
.map(|(name, _)| name.as_str())
.collect::<Vec<_>>()
.join(", ")
};
Ok(ToolResult {
success: false,
output: String::new(),
error: Some(format!(
"Skill '{}' not found. Available skills: {}",
skill_name, available_str
)),
})
}
}
}
fn list_skills_full(&self) -> anyhow::Result<ToolResult> {
let skills: Vec<_> = self
.skills_loader
.get_loaded_skills()
.into_iter()
.filter(|skill| {
self.allowed
.as_ref()
.is_none_or(|allowed| allowed.contains(&skill.name))
})
.collect();
if skills.is_empty() {
return Ok(ToolResult {
success: true,
output: "当前没有安装任何 skill".to_string(),
error: None,
});
}
let mut output = format!("可用 skill (共 {} 个):\n", skills.len());
for s in &skills {
let always_mark = if s.always { " [常驻]" } else { "" };
let path_str = s
.path
.as_ref()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| "".to_string());
output.push_str(&format!(
"- {}{}\n 简介: {}\n 路径: {}\n",
s.name, always_mark, s.description, path_str
));
}
Ok(ToolResult {
success: true,
output,
error: None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use tempfile::tempdir;
#[tokio::test]
async fn test_get_existing_skill() {
let temp_dir = tempdir().unwrap();
let skill_dir = temp_dir.path().join("test-skill");
std::fs::create_dir(&skill_dir).unwrap();
let mut skill_file = File::create(skill_dir.join("SKILL.md")).unwrap();
writeln!(skill_file, "---").unwrap();
writeln!(skill_file, "name: test-skill").unwrap();
writeln!(skill_file, "description: A test skill").unwrap();
writeln!(skill_file, "---").unwrap();
writeln!(skill_file, "# Test Skill").unwrap();
writeln!(skill_file, "This is the test content.").unwrap();
let loader = SkillsLoader::new_for_testing(
temp_dir.path().to_path_buf(),
PathBuf::from("/nonexistent"),
);
loader.load_skills();
let tool = GetSkillTool::new(Arc::new(loader));
let result = tool
.execute(json!({ "skill_name": "test-skill" }))
.await
.unwrap();
assert!(result.success);
assert!(result.output.contains("test-skill"));
assert!(result.output.contains("test content"));
}
#[tokio::test]
async fn test_get_nonexistent_skill() {
let loader = SkillsLoader::new();
let tool = GetSkillTool::new(Arc::new(loader));
let result = tool
.execute(json!({ "skill_name": "nonexistent" }))
.await
.unwrap();
assert!(!result.success);
assert!(result.error.is_some());
}
}