Compare commits
No commits in common. "644f5f91328ab4e8b7fab300501b7f92d2b001ba" and "58b37bb796a0fd7883dc16224b2280b751bb1609" have entirely different histories.
644f5f9132
...
58b37bb796
@ -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 | 否 | 是否只读代理 |
|
||||||
|
|
||||||
#### 模板变量
|
#### 模板变量
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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");
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>,
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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('"', """)
|
.replace('"', """)
|
||||||
.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,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",
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user