Compare commits

..

No commits in common. "644f5f91328ab4e8b7fab300501b7f92d2b001ba" and "58b37bb796a0fd7883dc16224b2280b751bb1609" have entirely different histories.

12 changed files with 29 additions and 352 deletions

View File

@ -540,6 +540,7 @@ prompt_template: |
注意: 你是一个只读代理,禁止执行任何修改操作。 注意: 你是一个只读代理,禁止执行任何修改操作。
allowed_tools: [read, bash, web_fetch] # 可选,覆盖默认工具白名单 allowed_tools: [read, bash, web_fetch] # 可选,覆盖默认工具白名单
max_execution_secs: 600 # 可选,覆盖默认执行时间 max_execution_secs: 600 # 可选,覆盖默认执行时间
read_only: true # 可选,标记为只读代理
--- ---
请重点关注: 请重点关注:
@ -557,6 +558,7 @@ max_execution_secs: 600 # 可选,覆盖默认执行时间
| `prompt_template` | string | 是 | 提示词模板,支持变量插值 | | `prompt_template` | string | 是 | 提示词模板,支持变量插值 |
| `allowed_tools` | array | 否 | 工具白名单,不指定时使用默认列表 | | `allowed_tools` | array | 否 | 工具白名单,不指定时使用默认列表 |
| `max_execution_secs` | integer | 否 | 最大执行时间(秒) | | `max_execution_secs` | integer | 否 | 最大执行时间(秒) |
| `read_only` | boolean | 否 | 是否只读代理 |
#### 模板变量 #### 模板变量

View File

@ -228,11 +228,11 @@ fn default_task_enabled() -> bool {
} }
fn default_task_max_execution_secs() -> u64 { fn default_task_max_execution_secs() -> u64 {
3600 // 60分钟 1200 // 20分钟
} }
fn default_task_explore_max_execution_secs() -> u64 { fn default_task_explore_max_execution_secs() -> u64 {
3600 // 60分钟 600 // 10分钟
} }
fn default_task_ttl_hours() -> u64 { fn default_task_ttl_hours() -> u64 {

View File

@ -86,7 +86,6 @@ impl GatewayState {
Arc::new(BusSessionMessageSender::new(bus.clone())), Arc::new(BusSessionMessageSender::new(bus.clone())),
std::collections::HashSet::new(), std::collections::HashSet::new(),
config.tools.task.clone(), config.tools.task.clone(),
config.subagents.clone(),
config.memory_maintenance.clone(), config.memory_maintenance.clone(),
session_ttl_hours, session_ttl_hours,
mcp_config, mcp_config,

View File

@ -4,7 +4,7 @@ use std::collections::{HashMap, HashSet};
use std::sync::Arc; use std::sync::Arc;
use crate::agent::AgentError; use crate::agent::AgentError;
use crate::config::{LLMProviderConfig, MemoryMaintenanceConfig, SubagentsConfig, TaskConfig}; use crate::config::{LLMProviderConfig, MemoryMaintenanceConfig, TaskConfig};
use crate::gateway::tool_registry_factory::ToolRegistryFactory; use crate::gateway::tool_registry_factory::ToolRegistryFactory;
use crate::mcp::McpInitializer; use crate::mcp::McpInitializer;
use crate::skills::SkillRuntime; use crate::skills::SkillRuntime;
@ -40,7 +40,6 @@ pub(crate) fn build_session_manager(
skills: Arc<SkillRuntime>, skills: Arc<SkillRuntime>,
disabled_tools: HashSet<String>, disabled_tools: HashSet<String>,
task_config: TaskConfig, task_config: TaskConfig,
subagents_config: SubagentsConfig,
maintenance_config: MemoryMaintenanceConfig, maintenance_config: MemoryMaintenanceConfig,
session_ttl_hours: Option<u64>, session_ttl_hours: Option<u64>,
mcp_config: crate::mcp::McpConfig, mcp_config: crate::mcp::McpConfig,
@ -55,7 +54,6 @@ pub(crate) fn build_session_manager(
Arc::new(NoopSessionMessageSender), Arc::new(NoopSessionMessageSender),
disabled_tools, disabled_tools,
task_config, task_config,
subagents_config,
maintenance_config, maintenance_config,
session_ttl_hours, session_ttl_hours,
mcp_config, mcp_config,
@ -73,7 +71,6 @@ pub(crate) fn build_session_manager_with_sender(
session_message_sender: Arc<dyn SessionMessageSender>, session_message_sender: Arc<dyn SessionMessageSender>,
disabled_tools: HashSet<String>, disabled_tools: HashSet<String>,
task_config: TaskConfig, task_config: TaskConfig,
subagents_config: SubagentsConfig,
maintenance_config: MemoryMaintenanceConfig, maintenance_config: MemoryMaintenanceConfig,
session_ttl_hours: Option<u64>, session_ttl_hours: Option<u64>,
mcp_config: crate::mcp::McpConfig, mcp_config: crate::mcp::McpConfig,
@ -124,54 +121,13 @@ pub(crate) fn build_session_manager_with_sender(
factory factory
}; };
// Wait for MCP connections and collect MCP tools for subagents
// This needs to happen before building subagent tools
let mut mcp_tools_for_subagents: Vec<crate::mcp::tool_adapter::McpToolWrapper> = Vec::new();
if mcp_initializer.is_enabled() {
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async {
// Wait for connections to complete
if let Err(e) = mcp_initializer.wait_for_connections().await {
tracing::error!(error = %e, "Failed to wait for MCP connections");
return;
}
// Collect MCP tools for subagents
if let Some(manager) = mcp_initializer.manager() {
let all_tools = manager.all_tools().await;
for (server_key, tool_info) in all_tools {
let wrapper = crate::mcp::tool_adapter::McpToolWrapper::new(
manager.clone(),
server_key.clone(),
tool_info,
);
mcp_tools_for_subagents.push(wrapper);
}
tracing::info!(
tool_count = mcp_tools_for_subagents.len(),
"Collected MCP tools for subagents"
);
}
})
});
}
// Create SubAgentRuntime (if task tool is enabled) // Create SubAgentRuntime (if task tool is enabled)
let (factory, task_repository): (_, Arc<dyn TaskRepository>) = if task_config.enabled { let (factory, task_repository): (_, Arc<dyn TaskRepository>) = if task_config.enabled {
let task_repository = Arc::new(InMemoryTaskRepository::new()); let task_repository = Arc::new(InMemoryTaskRepository::new());
// Build subagent tools with MCP tools let subagent_tools = Arc::new(factory.build_subagent_tools());
let subagent_tools = Arc::new(
factory.build_subagent_tools(
if mcp_tools_for_subagents.is_empty() {
None
} else {
Some(mcp_tools_for_subagents.clone())
}
)
);
// Create subagent catalog with discovery // Create subagent catalog with builtin definitions
let catalog = Arc::new(SubagentCatalog::discover(&subagents_config)); let catalog = Arc::new(SubagentCatalog::new());
let runtime_config = SubAgentRuntimeConfig { let runtime_config = SubAgentRuntimeConfig {
default_allowed_tools: task_config.allowed_tools.iter().cloned().collect(), default_allowed_tools: task_config.allowed_tools.iter().cloned().collect(),
@ -198,16 +154,14 @@ pub(crate) fn build_session_manager_with_sender(
// Build base tools // Build base tools
let mut tools = factory.build(); let mut tools = factory.build();
// Register MCP tools to main agent (async) // Register MCP tools (async)
// Note: MCP tools for subagents are already collected above // This waits briefly for connections, then registers available tools
if mcp_initializer.is_enabled() { if mcp_initializer.is_enabled() {
tokio::task::block_in_place(|| { tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async { tokio::runtime::Handle::current().block_on(async {
// Register pre-collected MCP tools if let Err(e) = mcp_initializer.register_tools(&mut tools).await {
for tool in mcp_tools_for_subagents { tracing::error!(error = %e, "Failed to register MCP tools");
tools.register(tool);
} }
tracing::info!("Registered MCP tools to main agent");
}) })
}); });
} }

View File

@ -493,7 +493,6 @@ impl SessionManager {
skills: Arc<SkillRuntime>, skills: Arc<SkillRuntime>,
disabled_tools: std::collections::HashSet<String>, disabled_tools: std::collections::HashSet<String>,
task_config: crate::config::TaskConfig, task_config: crate::config::TaskConfig,
subagents_config: crate::config::SubagentsConfig,
maintenance_config: crate::config::MemoryMaintenanceConfig, maintenance_config: crate::config::MemoryMaintenanceConfig,
session_ttl_hours: Option<u64>, session_ttl_hours: Option<u64>,
mcp_config: crate::mcp::McpConfig, mcp_config: crate::mcp::McpConfig,
@ -507,7 +506,6 @@ impl SessionManager {
skills, skills,
disabled_tools, disabled_tools,
task_config, task_config,
subagents_config,
maintenance_config, maintenance_config,
session_ttl_hours, session_ttl_hours,
mcp_config, mcp_config,
@ -970,7 +968,6 @@ mod tests {
Arc::new(SkillRuntime::default()), Arc::new(SkillRuntime::default()),
HashSet::new(), HashSet::new(),
crate::config::TaskConfig::default(), crate::config::TaskConfig::default(),
crate::config::SubagentsConfig::default(),
crate::config::MemoryMaintenanceConfig::default(), crate::config::MemoryMaintenanceConfig::default(),
Some(24), Some(24),
crate::mcp::McpConfig::default(), crate::mcp::McpConfig::default(),
@ -1026,7 +1023,6 @@ mod tests {
Arc::new(SkillRuntime::default()), Arc::new(SkillRuntime::default()),
HashSet::new(), HashSet::new(),
crate::config::TaskConfig::default(), crate::config::TaskConfig::default(),
crate::config::SubagentsConfig::default(),
crate::config::MemoryMaintenanceConfig::default(), crate::config::MemoryMaintenanceConfig::default(),
Some(24), Some(24),
crate::mcp::McpConfig::default(), crate::mcp::McpConfig::default(),
@ -1096,7 +1092,6 @@ mod tests {
Arc::new(SkillRuntime::default()), Arc::new(SkillRuntime::default()),
HashSet::new(), HashSet::new(),
crate::config::TaskConfig::default(), crate::config::TaskConfig::default(),
crate::config::SubagentsConfig::default(),
crate::config::MemoryMaintenanceConfig::default(), crate::config::MemoryMaintenanceConfig::default(),
Some(24), Some(24),
crate::mcp::McpConfig::default(), crate::mcp::McpConfig::default(),
@ -1184,7 +1179,6 @@ mod tests {
Arc::new(SkillRuntime::default()), Arc::new(SkillRuntime::default()),
HashSet::new(), HashSet::new(),
crate::config::TaskConfig::default(), crate::config::TaskConfig::default(),
crate::config::SubagentsConfig::default(),
crate::config::MemoryMaintenanceConfig::default(), crate::config::MemoryMaintenanceConfig::default(),
Some(24), Some(24),
crate::mcp::McpConfig::default(), crate::mcp::McpConfig::default(),
@ -1274,7 +1268,6 @@ mod tests {
Arc::new(SkillRuntime::default()), Arc::new(SkillRuntime::default()),
HashSet::new(), HashSet::new(),
crate::config::TaskConfig::default(), crate::config::TaskConfig::default(),
crate::config::SubagentsConfig::default(),
crate::config::MemoryMaintenanceConfig::default(), crate::config::MemoryMaintenanceConfig::default(),
Some(24), Some(24),
crate::mcp::McpConfig::default(), crate::mcp::McpConfig::default(),
@ -1363,7 +1356,6 @@ mod tests {
Arc::new(SkillRuntime::default()), Arc::new(SkillRuntime::default()),
HashSet::new(), HashSet::new(),
crate::config::TaskConfig::default(), crate::config::TaskConfig::default(),
crate::config::SubagentsConfig::default(),
crate::config::MemoryMaintenanceConfig::default(), crate::config::MemoryMaintenanceConfig::default(),
Some(24), Some(24),
crate::mcp::McpConfig::default(), crate::mcp::McpConfig::default(),
@ -1434,7 +1426,6 @@ mod tests {
Arc::new(SkillRuntime::default()), Arc::new(SkillRuntime::default()),
HashSet::new(), HashSet::new(),
crate::config::TaskConfig::default(), crate::config::TaskConfig::default(),
crate::config::SubagentsConfig::default(),
crate::config::MemoryMaintenanceConfig::default(), crate::config::MemoryMaintenanceConfig::default(),
Some(24), Some(24),
crate::mcp::McpConfig::default(), crate::mcp::McpConfig::default(),
@ -1514,7 +1505,6 @@ mod tests {
Arc::new(SkillRuntime::default()), Arc::new(SkillRuntime::default()),
HashSet::new(), HashSet::new(),
crate::config::TaskConfig::default(), crate::config::TaskConfig::default(),
crate::config::SubagentsConfig::default(),
crate::config::MemoryMaintenanceConfig::default(), crate::config::MemoryMaintenanceConfig::default(),
Some(24), Some(24),
crate::mcp::McpConfig::default(), crate::mcp::McpConfig::default(),
@ -1581,7 +1571,6 @@ mod tests {
Arc::new(SkillRuntime::default()), Arc::new(SkillRuntime::default()),
HashSet::new(), HashSet::new(),
crate::config::TaskConfig::default(), crate::config::TaskConfig::default(),
crate::config::SubagentsConfig::default(),
crate::config::MemoryMaintenanceConfig::default(), crate::config::MemoryMaintenanceConfig::default(),
Some(24), Some(24),
crate::mcp::McpConfig::default(), crate::mcp::McpConfig::default(),

View File

@ -142,11 +142,7 @@ impl ToolRegistryFactory {
} }
/// 构建子代理专用工具集(不包含 task 工具防止递归) /// 构建子代理专用工具集(不包含 task 工具防止递归)
/// 可选地包含 MCP 工具(通过 mcp_tools 参数传递) pub(crate) fn build_subagent_tools(&self) -> ToolRegistry {
pub(crate) fn build_subagent_tools(
&self,
mcp_tools: Option<Vec<crate::mcp::tool_adapter::McpToolWrapper>>,
) -> ToolRegistry {
let mut registry = ToolRegistry::new(); let mut registry = ToolRegistry::new();
// 基础工具 // 基础工具
@ -198,13 +194,6 @@ impl ToolRegistryFactory {
registry.register(SessionSendTool::new(self.session_message_sender.clone())); registry.register(SessionSendTool::new(self.session_message_sender.clone()));
} }
// 注册 MCP 工具(如果提供)
if let Some(mcp_tools) = mcp_tools {
for tool in mcp_tools {
registry.register(tool);
}
}
// 注意:不注册 task 工具,防止递归创建子代理 // 注意:不注册 task 工具,防止递归创建子代理
registry registry

View File

@ -9,7 +9,6 @@ use crate::mcp::client::McpClientManager;
use crate::tools::traits::{Tool as PicoBotTool, ToolResult}; use crate::tools::traits::{Tool as PicoBotTool, ToolResult};
/// Wrapper that adapts an MCP tool to PicoBot's Tool trait /// Wrapper that adapts an MCP tool to PicoBot's Tool trait
#[derive(Clone)]
pub struct McpToolWrapper { pub struct McpToolWrapper {
/// The MCP client manager /// The MCP client manager
manager: Arc<McpClientManager>, manager: Arc<McpClientManager>,

View File

@ -612,18 +612,10 @@ impl OpenAIProvider {
message message
} }
}).collect::<Vec<_>>(), }).collect::<Vec<_>>(),
"temperature": request.temperature.or(self.temperature).unwrap_or(0.7),
"max_tokens": request.max_tokens.or(self.max_tokens),
}); });
// 只有配置了才添加 temperature否则让模型使用默认值
if let Some(temp) = request.temperature.or(self.temperature) {
body["temperature"] = json!(temp);
}
// 只有配置了才添加 max_tokens
if let Some(tokens) = request.max_tokens.or(self.max_tokens) {
body["max_tokens"] = json!(tokens);
}
for (key, value) in self.request_model_extra() { for (key, value) in self.request_model_extra() {
body[key] = value.clone(); body[key] = value.clone();
} }

View File

@ -43,21 +43,8 @@ impl SubagentPromptBuilder {
/// 插值提示词模板 /// 插值提示词模板
fn interpolate_template(def: &SubagentDef, description: &str, prompt: &str) -> String { fn interpolate_template(def: &SubagentDef, description: &str, prompt: &str) -> String {
// 自定义子代理使用通用模板 let mut result = def
let base = if def.prompt_template.is_empty() { .prompt_template
"你是一个专注的子代理,正在执行一个独立任务。\n\n\
: {{description}}\n\n\
:\n\
1. \n\
2. 使\n\
3. \n\
4. \n\n\
: 访"
} else {
&def.prompt_template
};
let mut result = base
.replace("{{description}}", description) .replace("{{description}}", description)
.replace("{{prompt}}", prompt); .replace("{{prompt}}", prompt);
@ -101,6 +88,7 @@ mod tests {
body: None, body: None,
allowed_tools: None, allowed_tools: None,
max_execution_secs: None, max_execution_secs: None,
read_only: None,
source: SubagentSource::Builtin, source: SubagentSource::Builtin,
path: None, path: None,
} }

View File

@ -1,22 +1,19 @@
use std::collections::HashSet; use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use async_trait::async_trait; use async_trait::async_trait;
use serde::Deserialize;
use crate::agent::{AgentLoop, AgentRuntimeConfig, SystemPrompt, SystemPromptContext, SystemPromptProvider}; use crate::agent::{AgentLoop, AgentRuntimeConfig, SystemPrompt, SystemPromptContext, SystemPromptProvider};
use crate::bus::ChatMessage; use crate::bus::ChatMessage;
use crate::config::{LLMProviderConfig, SubagentsConfig}; use crate::config::LLMProviderConfig;
use crate::storage::ConversationRepository; use crate::storage::ConversationRepository;
use crate::tools::{ToolContext, ToolRegistry}; use crate::tools::{ToolContext, ToolRegistry};
use super::error::TaskError; use super::error::TaskError;
use super::prompt::{extract_summary, SubagentPromptBuilder}; use super::prompt::{extract_summary, SubagentPromptBuilder};
use super::repository::TaskRepository; use super::repository::TaskRepository;
use super::types::{SubagentDef, SubagentSource, TaskDefinition, TaskSession, TaskToolResult}; use super::types::{SubagentDef, TaskDefinition, TaskSession, TaskToolResult};
/// 子代理运行时配置 /// 子代理运行时配置
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -50,8 +47,8 @@ impl Default for SubAgentRuntimeConfig {
"skill_list".to_string(), "skill_list".to_string(),
"send_session_message".to_string(), // 用于进度通知 "send_session_message".to_string(), // 用于进度通知
]), ]),
default_max_execution_secs: 3600, // 60分钟 default_max_execution_secs: 1200, // 20分钟
explore_max_execution_secs: 3600, // 60分钟 explore_max_execution_secs: 600, // 10分钟
ttl_hours: 24, ttl_hours: 24,
skills_index: None, skills_index: None,
} }
@ -482,65 +479,6 @@ impl SubagentCatalog {
catalog catalog
} }
/// 从配置发现子代理(内置 + 文件系统自定义)
///
/// 发现顺序:先内置,后按 sources 配置顺序扫描目录
/// 后发现的同名定义会覆盖先发现的(项目覆盖用户)
pub fn discover(config: &SubagentsConfig) -> Self {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
Self::discover_with_cwd(config, &cwd)
}
fn discover_with_cwd(config: &SubagentsConfig, cwd: &Path) -> Self {
// 先内置作为基础
let mut merged: std::collections::HashMap<String, SubagentDef> = std::collections::HashMap::new();
merged.insert("general".to_string(), SubagentDef::builtin_general());
merged.insert("explore".to_string(), SubagentDef::builtin_explore());
tracing::debug!(cwd = %cwd.display(), "Discovering subagents from cwd");
// 按配置顺序扫描源目录
if config.enabled {
for source in source_order(&config.sources) {
let root = source_root(source, cwd);
tracing::debug!(source = ?source, root = ?root.as_ref().map(|p| p.display().to_string()), "Checking subagent source");
if let Some(root) = root {
if root.exists() {
tracing::info!(path = %root.display(), "Scanning subagents directory");
} else {
tracing::debug!(path = %root.display(), "Subagents directory does not exist, skipping");
}
for def in load_subagents_from_root(&root, source) {
if let Some(existing) = merged.get(&def.name) {
tracing::warn!(
subagent = %def.name,
old_source = ?existing.source,
new_source = ?def.source,
"Duplicate subagent name found; overriding with later source"
);
}
merged.insert(def.name.clone(), def);
}
}
}
} else {
tracing::debug!("Subagents discovery is disabled");
}
// 构建 catalog
let mut catalog = Self::default();
for def in merged.into_values() {
catalog.register(def);
}
tracing::info!(
discovered = catalog.definitions.len(),
"Subagents discovery completed"
);
catalog
}
/// 注册一个子代理定义(同名覆盖) /// 注册一个子代理定义(同名覆盖)
pub fn register(&mut self, def: SubagentDef) { pub fn register(&mut self, def: SubagentDef) {
self.definitions.insert(def.name.clone(), def); self.definitions.insert(def.name.clone(), def);
@ -595,178 +533,3 @@ fn xml_escape(s: &str) -> String {
.replace('"', "&quot;") .replace('"', "&quot;")
.replace('\'', "&apos;") .replace('\'', "&apos;")
} }
// ========== 自定义子代理发现 ==========
/// 源顺序解析
fn source_order(sources: &[String]) -> Vec<SubagentSource> {
let mut result = Vec::new();
for source in sources {
match source.as_str() {
"user" => {
if !result.contains(&SubagentSource::User) {
result.push(SubagentSource::User);
}
}
"project" => {
if !result.contains(&SubagentSource::Project) {
result.push(SubagentSource::Project);
}
}
unknown => {
tracing::warn!(source = %unknown, "Unknown subagents source ignored");
}
}
}
// 默认顺序:先 user 后 project项目覆盖用户
if result.is_empty() {
vec![SubagentSource::User, SubagentSource::Project]
} else {
result
}
}
/// 获取源目录根路径
fn source_root(source: SubagentSource, cwd: &Path) -> Option<std::path::PathBuf> {
match source {
SubagentSource::User => dirs::home_dir().map(|p| p.join(".picobot").join("subagents")),
SubagentSource::Project => Some(cwd.join(".picobot").join("subagents")),
SubagentSource::Builtin => None,
}
}
/// 子代理 frontmatter 结构
#[derive(Debug, Deserialize)]
struct SubagentFrontmatter {
#[serde(default)]
name: Option<String>,
description: String,
#[serde(default)]
prompt_template: Option<String>,
#[serde(default)]
allowed_tools: Option<Vec<String>>,
#[serde(default)]
max_execution_secs: Option<u64>,
}
/// 从根目录加载所有子代理
fn load_subagents_from_root(root: &Path, source: SubagentSource) -> Vec<SubagentDef> {
let mut out = Vec::new();
if !root.exists() {
tracing::debug!(path = %root.display(), "Subagents root directory does not exist");
return out;
}
tracing::debug!(path = %root.display(), "Reading subagents directory");
let entries = match fs::read_dir(root) {
Ok(entries) => entries,
Err(err) => {
tracing::warn!(path = %root.display(), error = %err, "Failed to read subagents directory");
return out;
}
};
let mut found_dirs = 0;
let mut found_files = 0;
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
tracing::debug!(path = %path.display(), "Skipping non-directory entry");
continue;
}
found_dirs += 1;
let subagent_md = path.join("SUBAGENT.md");
tracing::debug!(dir = %path.display(), subagent_file = %subagent_md.display(), "Checking subagent directory");
if !subagent_md.exists() {
tracing::debug!(path = %subagent_md.display(), "SUBAGENT.md not found");
continue;
}
found_files += 1;
match parse_subagent_file(&subagent_md, source) {
Ok(def) => {
tracing::info!(name = %def.name, path = %subagent_md.display(), "Loaded subagent");
out.push(def);
}
Err(err) => {
tracing::warn!(path = %subagent_md.display(), error = %err, "Skipping invalid subagent file");
}
}
}
tracing::debug!(path = %root.display(), dirs = found_dirs, files = found_files, loaded = out.len(), "Subagents scan completed");
out
}
/// 解析子代理文件
fn parse_subagent_file(path: &Path, source: SubagentSource) -> Result<SubagentDef, String> {
let content = fs::read_to_string(path)
.map_err(|e| format!("failed to read file: {}", e))?;
let (frontmatter_raw, body) = split_frontmatter(&content)
.ok_or_else(|| "missing YAML frontmatter block".to_string())?;
let frontmatter: SubagentFrontmatter = serde_yaml::from_str(frontmatter_raw)
.map_err(|e| format!("invalid YAML frontmatter: {}", e))?;
if frontmatter.description.trim().is_empty() {
return Err("description is required and cannot be empty".to_string());
}
// name 可选,默认使用目录名
let dir_name = path
.parent()
.and_then(|p| p.file_name())
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "unknown-subagent".to_string());
let name = frontmatter.name.unwrap_or(dir_name).trim().to_string();
let prompt_template = frontmatter.prompt_template.unwrap_or_default().trim().to_string();
let body_content = body.trim().to_string();
Ok(SubagentDef {
name,
description: frontmatter.description.trim().to_string(),
prompt_template,
body: if body_content.is_empty() { None } else { Some(body_content) },
allowed_tools: frontmatter.allowed_tools,
max_execution_secs: frontmatter.max_execution_secs,
source,
path: Some(path.to_path_buf()),
})
}
/// 分割 frontmatter 和 body
fn split_frontmatter(content: &str) -> Option<(&str, &str)> {
// 跳过开头的 ---
let content = content
.strip_prefix("---")
.or_else(|| content.strip_prefix("---"))?;
// 跳过 --- 后的换行符和可能的空行
let content = content.trim_start_matches('\r').trim_start_matches('\n');
// 找结束标记(容忍不同的换行符格式和前面的空行)
// 尝试多种可能的结束标记格式
let end_markers = ["\n---\n", "\n---", "\r\n---\r\n", "\r\n---"];
let mut idx = None;
let mut marker_len = 0;
for marker in end_markers {
if let Some(pos) = content.find(marker) {
idx = Some(pos);
marker_len = marker.len();
break;
}
}
let idx = idx?;
let frontmatter = &content[..idx];
let body = &content[idx + marker_len..];
let body = body.trim_start_matches('\r').trim_start_matches('\n');
Some((frontmatter, body))
}

View File

@ -27,13 +27,11 @@ impl Tool for TaskTool {
fn description(&self) -> &str { fn description(&self) -> &str {
"Launch a specialized subagent to handle complex, multi-step tasks. \ "Launch a specialized subagent to handle complex, multi-step tasks. \
Subagents run in isolated contexts and can work in parallel. \ Subagents run in isolated contexts and can work in parallel. \
Use 'general' type for complex tasks, 'explore' type for read-only exploration. \
You can resume a previous task by providing its task_id." You can resume a previous task by providing its task_id."
} }
fn parameters_schema(&self) -> serde_json::Value { fn parameters_schema(&self) -> serde_json::Value {
let types = self.runtime.available_subagent_names();
let types_array: Vec<serde_json::Value> = types.into_iter().map(|t| json!(t)).collect();
json!({ json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -48,9 +46,9 @@ impl Tool for TaskTool {
}, },
"subagent_type": { "subagent_type": {
"type": "string", "type": "string",
"enum": types_array, "enum": ["general", "explore"],
"default": "general", "default": "general",
"description": "Type of subagent to use for the task" "description": "Type of subagent: 'general' for complex multi-step tasks, 'explore' for read-only search/exploration"
}, },
"task_id": { "task_id": {
"type": "string", "type": "string",

View File

@ -49,6 +49,8 @@ pub struct SubagentDef {
pub allowed_tools: Option<Vec<String>>, pub allowed_tools: Option<Vec<String>>,
/// 最大执行时间None 表示使用默认 /// 最大执行时间None 表示使用默认
pub max_execution_secs: Option<u64>, pub max_execution_secs: Option<u64>,
/// 是否只读代理
pub read_only: Option<bool>,
/// 来源 /// 来源
pub source: SubagentSource, pub source: SubagentSource,
/// 文件路径(仅自定义类型) /// 文件路径(仅自定义类型)
@ -65,6 +67,7 @@ impl SubagentDef {
body: None, body: None,
allowed_tools: None, allowed_tools: None,
max_execution_secs: None, max_execution_secs: None,
read_only: Some(false),
source: SubagentSource::Builtin, source: SubagentSource::Builtin,
path: None, path: None,
} }
@ -79,6 +82,7 @@ impl SubagentDef {
body: None, body: None,
allowed_tools: None, allowed_tools: None,
max_execution_secs: None, max_execution_secs: None,
read_only: Some(true),
source: SubagentSource::Builtin, source: SubagentSource::Builtin,
path: None, path: None,
} }