Compare commits
5 Commits
58b37bb796
...
644f5f9132
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
644f5f9132 | ||
|
|
861aa04690 | ||
|
|
c41c2a1d2e | ||
|
|
b3dc207ad1 | ||
|
|
86d48a3ec0 |
@ -540,7 +540,6 @@ prompt_template: |
|
||||
注意: 你是一个只读代理,禁止执行任何修改操作。
|
||||
allowed_tools: [read, bash, web_fetch] # 可选,覆盖默认工具白名单
|
||||
max_execution_secs: 600 # 可选,覆盖默认执行时间
|
||||
read_only: true # 可选,标记为只读代理
|
||||
---
|
||||
|
||||
请重点关注:
|
||||
@ -558,7 +557,6 @@ read_only: true # 可选,标记为只读代理
|
||||
| `prompt_template` | string | 是 | 提示词模板,支持变量插值 |
|
||||
| `allowed_tools` | array | 否 | 工具白名单,不指定时使用默认列表 |
|
||||
| `max_execution_secs` | integer | 否 | 最大执行时间(秒) |
|
||||
| `read_only` | boolean | 否 | 是否只读代理 |
|
||||
|
||||
#### 模板变量
|
||||
|
||||
|
||||
@ -228,11 +228,11 @@ fn default_task_enabled() -> bool {
|
||||
}
|
||||
|
||||
fn default_task_max_execution_secs() -> u64 {
|
||||
1200 // 20分钟
|
||||
3600 // 60分钟
|
||||
}
|
||||
|
||||
fn default_task_explore_max_execution_secs() -> u64 {
|
||||
600 // 10分钟
|
||||
3600 // 60分钟
|
||||
}
|
||||
|
||||
fn default_task_ttl_hours() -> u64 {
|
||||
|
||||
@ -86,6 +86,7 @@ impl GatewayState {
|
||||
Arc::new(BusSessionMessageSender::new(bus.clone())),
|
||||
std::collections::HashSet::new(),
|
||||
config.tools.task.clone(),
|
||||
config.subagents.clone(),
|
||||
config.memory_maintenance.clone(),
|
||||
session_ttl_hours,
|
||||
mcp_config,
|
||||
|
||||
@ -4,7 +4,7 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::agent::AgentError;
|
||||
use crate::config::{LLMProviderConfig, MemoryMaintenanceConfig, TaskConfig};
|
||||
use crate::config::{LLMProviderConfig, MemoryMaintenanceConfig, SubagentsConfig, TaskConfig};
|
||||
use crate::gateway::tool_registry_factory::ToolRegistryFactory;
|
||||
use crate::mcp::McpInitializer;
|
||||
use crate::skills::SkillRuntime;
|
||||
@ -40,6 +40,7 @@ pub(crate) fn build_session_manager(
|
||||
skills: Arc<SkillRuntime>,
|
||||
disabled_tools: HashSet<String>,
|
||||
task_config: TaskConfig,
|
||||
subagents_config: SubagentsConfig,
|
||||
maintenance_config: MemoryMaintenanceConfig,
|
||||
session_ttl_hours: Option<u64>,
|
||||
mcp_config: crate::mcp::McpConfig,
|
||||
@ -54,6 +55,7 @@ pub(crate) fn build_session_manager(
|
||||
Arc::new(NoopSessionMessageSender),
|
||||
disabled_tools,
|
||||
task_config,
|
||||
subagents_config,
|
||||
maintenance_config,
|
||||
session_ttl_hours,
|
||||
mcp_config,
|
||||
@ -71,6 +73,7 @@ pub(crate) fn build_session_manager_with_sender(
|
||||
session_message_sender: Arc<dyn SessionMessageSender>,
|
||||
disabled_tools: HashSet<String>,
|
||||
task_config: TaskConfig,
|
||||
subagents_config: SubagentsConfig,
|
||||
maintenance_config: MemoryMaintenanceConfig,
|
||||
session_ttl_hours: Option<u64>,
|
||||
mcp_config: crate::mcp::McpConfig,
|
||||
@ -121,13 +124,54 @@ pub(crate) fn build_session_manager_with_sender(
|
||||
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)
|
||||
let (factory, task_repository): (_, Arc<dyn TaskRepository>) = if task_config.enabled {
|
||||
let task_repository = Arc::new(InMemoryTaskRepository::new());
|
||||
let subagent_tools = Arc::new(factory.build_subagent_tools());
|
||||
// Build subagent tools with MCP 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 builtin definitions
|
||||
let catalog = Arc::new(SubagentCatalog::new());
|
||||
// Create subagent catalog with discovery
|
||||
let catalog = Arc::new(SubagentCatalog::discover(&subagents_config));
|
||||
|
||||
let runtime_config = SubAgentRuntimeConfig {
|
||||
default_allowed_tools: task_config.allowed_tools.iter().cloned().collect(),
|
||||
@ -154,14 +198,16 @@ pub(crate) fn build_session_manager_with_sender(
|
||||
// Build base tools
|
||||
let mut tools = factory.build();
|
||||
|
||||
// Register MCP tools (async)
|
||||
// This waits briefly for connections, then registers available tools
|
||||
// Register MCP tools to main agent (async)
|
||||
// Note: MCP tools for subagents are already collected above
|
||||
if mcp_initializer.is_enabled() {
|
||||
tokio::task::block_in_place(|| {
|
||||
tokio::runtime::Handle::current().block_on(async {
|
||||
if let Err(e) = mcp_initializer.register_tools(&mut tools).await {
|
||||
tracing::error!(error = %e, "Failed to register MCP tools");
|
||||
// Register pre-collected MCP tools
|
||||
for tool in mcp_tools_for_subagents {
|
||||
tools.register(tool);
|
||||
}
|
||||
tracing::info!("Registered MCP tools to main agent");
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@ -493,6 +493,7 @@ impl SessionManager {
|
||||
skills: Arc<SkillRuntime>,
|
||||
disabled_tools: std::collections::HashSet<String>,
|
||||
task_config: crate::config::TaskConfig,
|
||||
subagents_config: crate::config::SubagentsConfig,
|
||||
maintenance_config: crate::config::MemoryMaintenanceConfig,
|
||||
session_ttl_hours: Option<u64>,
|
||||
mcp_config: crate::mcp::McpConfig,
|
||||
@ -506,6 +507,7 @@ impl SessionManager {
|
||||
skills,
|
||||
disabled_tools,
|
||||
task_config,
|
||||
subagents_config,
|
||||
maintenance_config,
|
||||
session_ttl_hours,
|
||||
mcp_config,
|
||||
@ -968,6 +970,7 @@ mod tests {
|
||||
Arc::new(SkillRuntime::default()),
|
||||
HashSet::new(),
|
||||
crate::config::TaskConfig::default(),
|
||||
crate::config::SubagentsConfig::default(),
|
||||
crate::config::MemoryMaintenanceConfig::default(),
|
||||
Some(24),
|
||||
crate::mcp::McpConfig::default(),
|
||||
@ -1023,6 +1026,7 @@ mod tests {
|
||||
Arc::new(SkillRuntime::default()),
|
||||
HashSet::new(),
|
||||
crate::config::TaskConfig::default(),
|
||||
crate::config::SubagentsConfig::default(),
|
||||
crate::config::MemoryMaintenanceConfig::default(),
|
||||
Some(24),
|
||||
crate::mcp::McpConfig::default(),
|
||||
@ -1092,6 +1096,7 @@ mod tests {
|
||||
Arc::new(SkillRuntime::default()),
|
||||
HashSet::new(),
|
||||
crate::config::TaskConfig::default(),
|
||||
crate::config::SubagentsConfig::default(),
|
||||
crate::config::MemoryMaintenanceConfig::default(),
|
||||
Some(24),
|
||||
crate::mcp::McpConfig::default(),
|
||||
@ -1179,6 +1184,7 @@ mod tests {
|
||||
Arc::new(SkillRuntime::default()),
|
||||
HashSet::new(),
|
||||
crate::config::TaskConfig::default(),
|
||||
crate::config::SubagentsConfig::default(),
|
||||
crate::config::MemoryMaintenanceConfig::default(),
|
||||
Some(24),
|
||||
crate::mcp::McpConfig::default(),
|
||||
@ -1268,6 +1274,7 @@ mod tests {
|
||||
Arc::new(SkillRuntime::default()),
|
||||
HashSet::new(),
|
||||
crate::config::TaskConfig::default(),
|
||||
crate::config::SubagentsConfig::default(),
|
||||
crate::config::MemoryMaintenanceConfig::default(),
|
||||
Some(24),
|
||||
crate::mcp::McpConfig::default(),
|
||||
@ -1356,6 +1363,7 @@ mod tests {
|
||||
Arc::new(SkillRuntime::default()),
|
||||
HashSet::new(),
|
||||
crate::config::TaskConfig::default(),
|
||||
crate::config::SubagentsConfig::default(),
|
||||
crate::config::MemoryMaintenanceConfig::default(),
|
||||
Some(24),
|
||||
crate::mcp::McpConfig::default(),
|
||||
@ -1426,6 +1434,7 @@ mod tests {
|
||||
Arc::new(SkillRuntime::default()),
|
||||
HashSet::new(),
|
||||
crate::config::TaskConfig::default(),
|
||||
crate::config::SubagentsConfig::default(),
|
||||
crate::config::MemoryMaintenanceConfig::default(),
|
||||
Some(24),
|
||||
crate::mcp::McpConfig::default(),
|
||||
@ -1505,6 +1514,7 @@ mod tests {
|
||||
Arc::new(SkillRuntime::default()),
|
||||
HashSet::new(),
|
||||
crate::config::TaskConfig::default(),
|
||||
crate::config::SubagentsConfig::default(),
|
||||
crate::config::MemoryMaintenanceConfig::default(),
|
||||
Some(24),
|
||||
crate::mcp::McpConfig::default(),
|
||||
@ -1571,6 +1581,7 @@ mod tests {
|
||||
Arc::new(SkillRuntime::default()),
|
||||
HashSet::new(),
|
||||
crate::config::TaskConfig::default(),
|
||||
crate::config::SubagentsConfig::default(),
|
||||
crate::config::MemoryMaintenanceConfig::default(),
|
||||
Some(24),
|
||||
crate::mcp::McpConfig::default(),
|
||||
|
||||
@ -142,7 +142,11 @@ impl ToolRegistryFactory {
|
||||
}
|
||||
|
||||
/// 构建子代理专用工具集(不包含 task 工具防止递归)
|
||||
pub(crate) fn build_subagent_tools(&self) -> ToolRegistry {
|
||||
/// 可选地包含 MCP 工具(通过 mcp_tools 参数传递)
|
||||
pub(crate) fn build_subagent_tools(
|
||||
&self,
|
||||
mcp_tools: Option<Vec<crate::mcp::tool_adapter::McpToolWrapper>>,
|
||||
) -> ToolRegistry {
|
||||
let mut registry = ToolRegistry::new();
|
||||
|
||||
// 基础工具
|
||||
@ -194,6 +198,13 @@ impl ToolRegistryFactory {
|
||||
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 工具,防止递归创建子代理
|
||||
|
||||
registry
|
||||
|
||||
@ -9,6 +9,7 @@ use crate::mcp::client::McpClientManager;
|
||||
use crate::tools::traits::{Tool as PicoBotTool, ToolResult};
|
||||
|
||||
/// Wrapper that adapts an MCP tool to PicoBot's Tool trait
|
||||
#[derive(Clone)]
|
||||
pub struct McpToolWrapper {
|
||||
/// The MCP client manager
|
||||
manager: Arc<McpClientManager>,
|
||||
|
||||
@ -612,10 +612,18 @@ impl OpenAIProvider {
|
||||
message
|
||||
}
|
||||
}).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() {
|
||||
body[key] = value.clone();
|
||||
}
|
||||
|
||||
@ -43,8 +43,21 @@ impl SubagentPromptBuilder {
|
||||
|
||||
/// 插值提示词模板
|
||||
fn interpolate_template(def: &SubagentDef, description: &str, prompt: &str) -> String {
|
||||
let mut result = def
|
||||
.prompt_template
|
||||
// 自定义子代理使用通用模板
|
||||
let base = if def.prompt_template.is_empty() {
|
||||
"你是一个专注的子代理,正在执行一个独立任务。\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("{{prompt}}", prompt);
|
||||
|
||||
@ -88,7 +101,6 @@ mod tests {
|
||||
body: None,
|
||||
allowed_tools: None,
|
||||
max_execution_secs: None,
|
||||
read_only: None,
|
||||
source: SubagentSource::Builtin,
|
||||
path: None,
|
||||
}
|
||||
|
||||
@ -1,19 +1,22 @@
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::agent::{AgentLoop, AgentRuntimeConfig, SystemPrompt, SystemPromptContext, SystemPromptProvider};
|
||||
use crate::bus::ChatMessage;
|
||||
use crate::config::LLMProviderConfig;
|
||||
use crate::config::{LLMProviderConfig, SubagentsConfig};
|
||||
use crate::storage::ConversationRepository;
|
||||
use crate::tools::{ToolContext, ToolRegistry};
|
||||
|
||||
use super::error::TaskError;
|
||||
use super::prompt::{extract_summary, SubagentPromptBuilder};
|
||||
use super::repository::TaskRepository;
|
||||
use super::types::{SubagentDef, TaskDefinition, TaskSession, TaskToolResult};
|
||||
use super::types::{SubagentDef, SubagentSource, TaskDefinition, TaskSession, TaskToolResult};
|
||||
|
||||
/// 子代理运行时配置
|
||||
#[derive(Debug, Clone)]
|
||||
@ -47,8 +50,8 @@ impl Default for SubAgentRuntimeConfig {
|
||||
"skill_list".to_string(),
|
||||
"send_session_message".to_string(), // 用于进度通知
|
||||
]),
|
||||
default_max_execution_secs: 1200, // 20分钟
|
||||
explore_max_execution_secs: 600, // 10分钟
|
||||
default_max_execution_secs: 3600, // 60分钟
|
||||
explore_max_execution_secs: 3600, // 60分钟
|
||||
ttl_hours: 24,
|
||||
skills_index: None,
|
||||
}
|
||||
@ -479,6 +482,65 @@ impl SubagentCatalog {
|
||||
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) {
|
||||
self.definitions.insert(def.name.clone(), def);
|
||||
@ -533,3 +595,178 @@ fn xml_escape(s: &str) -> String {
|
||||
.replace('"', """)
|
||||
.replace('\'', "'")
|
||||
}
|
||||
|
||||
// ========== 自定义子代理发现 ==========
|
||||
|
||||
/// 源顺序解析
|
||||
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))
|
||||
}
|
||||
|
||||
@ -27,11 +27,13 @@ impl Tool for TaskTool {
|
||||
fn description(&self) -> &str {
|
||||
"Launch a specialized subagent to handle complex, multi-step tasks. \
|
||||
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."
|
||||
}
|
||||
|
||||
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!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -46,9 +48,9 @@ impl Tool for TaskTool {
|
||||
},
|
||||
"subagent_type": {
|
||||
"type": "string",
|
||||
"enum": ["general", "explore"],
|
||||
"enum": types_array,
|
||||
"default": "general",
|
||||
"description": "Type of subagent: 'general' for complex multi-step tasks, 'explore' for read-only search/exploration"
|
||||
"description": "Type of subagent to use for the task"
|
||||
},
|
||||
"task_id": {
|
||||
"type": "string",
|
||||
|
||||
@ -49,8 +49,6 @@ pub struct SubagentDef {
|
||||
pub allowed_tools: Option<Vec<String>>,
|
||||
/// 最大执行时间(秒),None 表示使用默认
|
||||
pub max_execution_secs: Option<u64>,
|
||||
/// 是否只读代理
|
||||
pub read_only: Option<bool>,
|
||||
/// 来源
|
||||
pub source: SubagentSource,
|
||||
/// 文件路径(仅自定义类型)
|
||||
@ -67,7 +65,6 @@ impl SubagentDef {
|
||||
body: None,
|
||||
allowed_tools: None,
|
||||
max_execution_secs: None,
|
||||
read_only: Some(false),
|
||||
source: SubagentSource::Builtin,
|
||||
path: None,
|
||||
}
|
||||
@ -82,7 +79,6 @@ impl SubagentDef {
|
||||
body: None,
|
||||
allowed_tools: None,
|
||||
max_execution_secs: None,
|
||||
read_only: Some(true),
|
||||
source: SubagentSource::Builtin,
|
||||
path: None,
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user