feat: 添加子代理配置,支持自定义子代理定义和运行时管理
This commit is contained in:
parent
b571d7b7b3
commit
9ae2813c20
145
README.md
145
README.md
@ -498,11 +498,108 @@ tools 配置示例:
|
||||
- shell - 执行 shell 命令(Windows PowerShell/Cmd)
|
||||
- http_request - HTTP 请求
|
||||
- web_fetch - 网页抓取
|
||||
- task - 创建和管理子代理
|
||||
- task - 创建和管理子代理,支持内置类型(general/explore)和用户自定义类型
|
||||
|
||||
注意:bash 和 shell 是同一个工具在不同平台上的名称,运行时自动检测。
|
||||
|
||||
## 8. 工具机制
|
||||
## 8. 子代理系统
|
||||
|
||||
PicoBot 支持通过 `task` 工具创建子代理来处理复杂多步骤任务。子代理在一个独立的执行上下文中运行,拥有独立的会话历史和工具权限。
|
||||
|
||||
### 8.1 内置子代理类型
|
||||
|
||||
- **general**:通用型子代理,适合处理复杂多步骤任务。可以使用读写文件、执行命令、HTTP 请求等完整工具集。
|
||||
- **explore**:探索型子代理,用于代码库探索和信息收集。只使用只读工具,禁止任何写操作。
|
||||
|
||||
### 8.2 自定义子代理
|
||||
|
||||
用户可以在文件系统上定义新的子代理类型,类似于技能的加载方式。子代理定义文件采用 YAML frontmatter + body 的格式。
|
||||
|
||||
#### 定义位置
|
||||
|
||||
按从低到高优先级合并,后加载来源可覆盖同名定义:
|
||||
|
||||
- 用户级:`~/.picobot/subagents/*/SUBAGENT.md`
|
||||
- 项目级:`.picobot/subagents/*/SUBAGENT.md`
|
||||
|
||||
#### SUBAGENT.md 格式
|
||||
|
||||
```md
|
||||
---
|
||||
name: code-review # 可选,省略时使用目录名
|
||||
description: 代码审查代理,用于检查代码质量和安全问题
|
||||
prompt_template: |
|
||||
你是一个专业的代码审查代理。
|
||||
任务描述: {{description}}
|
||||
|
||||
你应该:
|
||||
1. 检查代码质量和潜在问题
|
||||
2. 提出改进建议
|
||||
3. 完成后给出简洁的总结
|
||||
|
||||
注意: 你是一个只读代理,禁止执行任何修改操作。
|
||||
allowed_tools: [read, bash, web_fetch] # 可选,覆盖默认工具白名单
|
||||
max_execution_secs: 600 # 可选,覆盖默认执行时间
|
||||
read_only: true # 可选,标记为只读代理
|
||||
---
|
||||
|
||||
请重点关注:
|
||||
- SQL 注入风险
|
||||
- XSS 漏洞
|
||||
- 权限校验缺失
|
||||
```
|
||||
|
||||
#### 字段说明
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `name` | string | 否 | 子代理名称,默认取目录名 |
|
||||
| `description` | string | 是 | 简短描述,用于 agent 选择 |
|
||||
| `prompt_template` | string | 是 | 提示词模板,支持变量插值 |
|
||||
| `allowed_tools` | array | 否 | 工具白名单,不指定时使用默认列表 |
|
||||
| `max_execution_secs` | integer | 否 | 最大执行时间(秒) |
|
||||
| `read_only` | boolean | 否 | 是否只读代理 |
|
||||
|
||||
#### 模板变量
|
||||
|
||||
`prompt_template` 支持以下变量插值:
|
||||
|
||||
- `{{description}}`:任务描述(来自 `task` 工具的 description 参数)
|
||||
- `{{prompt}}`:详细指令(来自 `task` 工具的 prompt 参数)
|
||||
|
||||
文件 body 部分(frontmatter 之后的内容)会追加到提示词模板末尾,用于提供更详细的补充指令。
|
||||
|
||||
#### 使用方式
|
||||
|
||||
创建子代理后,通过 `task` 工具的 `subagent_type` 参数指定类型:
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "审查 src/auth 目录下的代码",
|
||||
"prompt": "检查安全漏洞和错误处理",
|
||||
"subagent_type": "code-review"
|
||||
}
|
||||
```
|
||||
|
||||
如果指定的子代理类型不存在,系统会自动回退到 `general` 类型。
|
||||
|
||||
### 8.3 子代理配置
|
||||
|
||||
```json
|
||||
{
|
||||
"subagents": {
|
||||
"enabled": true,
|
||||
"sources": ["user", "project"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `enabled` | `true` | 是否启用自定义子代理发现 |
|
||||
| `sources` | `["user", "project"]` | 定义来源优先级 |
|
||||
|
||||
## 9. 工具机制
|
||||
|
||||
PicoBot 的 Agent 是围绕工具调用构建的。当前默认注册的工具包括:
|
||||
|
||||
@ -520,7 +617,7 @@ PicoBot 的 Agent 是围绕工具调用构建的。当前默认注册的工具
|
||||
- bash / shell:执行 shell 命令(同一工具,Unix 下名称为 bash,Windows 下名称为 shell)
|
||||
- http_request:发起 HTTP 请求
|
||||
- web_fetch:抓取网页正文
|
||||
- task:创建和管理子代理
|
||||
- task:创建和管理子代理,支持内置类型(general/explore)和用户自定义类型
|
||||
|
||||
其中:
|
||||
|
||||
@ -530,9 +627,9 @@ PicoBot 的 Agent 是围绕工具调用构建的。当前默认注册的工具
|
||||
- skill_activate 负责把具体技能正文注入当前任务上下文
|
||||
- skill_manage 整合了技能列出与管理功能,支持运行时创建、更新、删除和批量禁用
|
||||
- bash / shell / http_request / web_fetch 让 Agent 具备更强的外部交互能力(bash 和 shell 是同一工具在不同平台的名称)
|
||||
- task 允许 Agent 创建独立上下文的子代理来处理复杂多步骤任务,支持 general 和 explore 两种类型
|
||||
- task 允许 Agent 创建独立上下文的子代理来处理复杂多步骤任务,支持内置类型(general/explore)和用户自定义类型
|
||||
|
||||
### 8.1 MCP 工具集成
|
||||
### 9.1 MCP 工具集成
|
||||
|
||||
PicoBot 支持通过 MCP (Model Context Protocol) 扩展工具能力,可以连接外部 MCP servers 并自动发现其提供的工具。配置格式兼容 Claude Desktop / Cursor。
|
||||
|
||||
@ -609,18 +706,18 @@ MCP 工具会自动注册到 ToolRegistry,命名格式为 `mcp_{server_key}_{t
|
||||
- 通过 Tool trait 适配器接入,无需修改核心代码
|
||||
- 连接失败不影响 Gateway 运行
|
||||
|
||||
## 9. 调度器机制
|
||||
## 10. 调度器机制
|
||||
|
||||
PicoBot 带有一个基于 SQLite 的调度器,而不是纯内存或 JSON 文件驱动的任务系统。
|
||||
|
||||
### 9.1 支持的调度类型
|
||||
### 10.1 支持的调度类型
|
||||
|
||||
- delay:延迟执行一次
|
||||
- interval:固定间隔执行
|
||||
- at:某个绝对时间执行一次
|
||||
- cron:cron 表达式调度
|
||||
|
||||
### 9.2 支持的任务类型
|
||||
### 10.2 支持的任务类型
|
||||
|
||||
- internal_event:内部事件
|
||||
- outbound_message:直接向目标通道发消息
|
||||
@ -643,7 +740,7 @@ silent_agent_task 和 agent_task 使用同一套 Agent 执行能力,但路由
|
||||
- 执行失败时会向主 chat 发送一条失败通知,便于用户感知异常
|
||||
- 后台任务的历史、压缩和会话内上下文会留在独立会话中,不污染主会话
|
||||
|
||||
### 9.3 运行时管理
|
||||
### 10.3 运行时管理
|
||||
|
||||
通过 scheduler_manage 可以进行:
|
||||
|
||||
@ -737,22 +834,22 @@ silent_agent_task 和 agent_task 使用同一套 Agent 执行能力,但路由
|
||||
- agent_task:用户需要直接收到结果,例如日报提醒、定时播报、定时外发通知
|
||||
- silent_agent_task:任务需要长期积累独立上下文或后台整理材料,但不应污染主会话,例如周报草稿整理、周期性资料汇总、后台分析任务
|
||||
|
||||
## 10. 渠道与运行方式
|
||||
## 11. 渠道与运行方式
|
||||
|
||||
### 10.1 当前支持的通道
|
||||
### 11.1 当前支持的通道
|
||||
|
||||
- WebSocket CLI 客户端
|
||||
- 飞书通道
|
||||
- 微信通道
|
||||
|
||||
### 10.2 Gateway 接口
|
||||
### 11.2 Gateway 接口
|
||||
|
||||
网关当前暴露:
|
||||
|
||||
- /health:健康检查
|
||||
- /ws:CLI 客户端连接入口
|
||||
|
||||
### 10.3 CLI 使用方式
|
||||
### 11.3 CLI 使用方式
|
||||
|
||||
程序提供两个主命令:
|
||||
|
||||
@ -779,7 +876,7 @@ CLI 中已实现的交互命令包括:
|
||||
- /clear
|
||||
- /quit
|
||||
|
||||
## 11. 配置说明
|
||||
## 12. 配置说明
|
||||
|
||||
配置默认从以下位置加载:
|
||||
|
||||
@ -846,9 +943,9 @@ CLI 中已实现的交互命令包括:
|
||||
- tools:工具启用/禁用配置(通过 disabled 列表指定禁用的工具)
|
||||
- time.timezone:时区,默认应使用 IANA 时区名,例如 Asia/Shanghai
|
||||
|
||||
## 12. 快速开始
|
||||
## 13. 快速开始
|
||||
|
||||
### 12.1 准备配置
|
||||
### 13.1 准备配置
|
||||
|
||||
1. 复制并修改 config.json,或把配置放到 ~/.picobot/config.json
|
||||
2. 配置好 Provider 的 base_url、api_key、model_id
|
||||
@ -894,13 +991,13 @@ CLI 中已实现的交互命令包括:
|
||||
}
|
||||
```
|
||||
|
||||
### 12.2 启动网关
|
||||
### 13.2 启动网关
|
||||
|
||||
```bash
|
||||
cargo run -- gateway
|
||||
```
|
||||
|
||||
### 12.3 启动本地 CLI
|
||||
### 13.3 启动本地 CLI
|
||||
|
||||
```bash
|
||||
cargo run -- agent
|
||||
@ -918,13 +1015,13 @@ ws://127.0.0.1:19876/ws
|
||||
cargo run -- agent --gateway-url ws://127.0.0.1:19876/ws
|
||||
```
|
||||
|
||||
### 12.4 检查服务状态
|
||||
### 13.4 检查服务状态
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:19876/health
|
||||
```
|
||||
|
||||
## 13. 目录结构
|
||||
## 14. 目录结构
|
||||
|
||||
```text
|
||||
PicoBot/
|
||||
@ -947,7 +1044,7 @@ PicoBot/
|
||||
│ ├── skills/ # 技能运行时
|
||||
│ ├── storage/ # SQLite 持久化(存储、端口、记录、错误)
|
||||
│ ├── text/ # 文本处理工具
|
||||
│ └── tools/ # 内置工具集合
|
||||
│ └── tools/ # 内置工具集合(含 task 子代理系统)
|
||||
├── docs/
|
||||
│ ├── IMPLEMENTATION_LOG.md
|
||||
│ └── PERSISTENCE.md
|
||||
@ -955,7 +1052,7 @@ PicoBot/
|
||||
└── config.json
|
||||
```
|
||||
|
||||
## 14. 测试与维护建议
|
||||
## 15. 测试与维护建议
|
||||
|
||||
当前 tests 目录中已经包含 Provider 集成测试和工具调用相关测试,但部分测试依赖外部 API Key,需要先准备 tests/test.env。
|
||||
|
||||
@ -969,13 +1066,13 @@ PicoBot/
|
||||
- src/bus/message.rs:消息结构变更(如 OutboundMessage 新增 session_id)
|
||||
- src/command/handlers/:命令处理器实现
|
||||
|
||||
## 15. 总结
|
||||
## 16. 总结
|
||||
|
||||
PicoBot 当前已经具备一个可长期运行 Agent 系统的关键组件:
|
||||
|
||||
- 有入口:Gateway + Channel
|
||||
- 有状态:SQLite + Session 恢复
|
||||
- 有能力:工具调用 + 技能系统 + MCP 扩展
|
||||
- 有能力:工具调用 + 技能系统 + MCP 扩展 + 可自定义子代理
|
||||
- 有记忆:长期记忆 + 自动维护摘要
|
||||
- 有计划:Scheduler + agent_task
|
||||
|
||||
|
||||
@ -78,6 +78,7 @@ impl InitWizard {
|
||||
memory_maintenance: crate::config::MemoryMaintenanceConfig::default(),
|
||||
mcp_servers: HashMap::new(),
|
||||
image_context: crate::config::ImageContextConfig::default(),
|
||||
subagents: crate::config::SubagentsConfig::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -830,6 +831,7 @@ impl InitWizard {
|
||||
memory_maintenance: existing.memory_maintenance.clone(),
|
||||
mcp_servers: existing.mcp_servers.clone(),
|
||||
image_context: existing.image_context.clone(),
|
||||
subagents: existing.subagents.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -33,6 +33,8 @@ pub struct Config {
|
||||
pub mcp_servers: HashMap<String, crate::mcp::McpServerConfig>,
|
||||
#[serde(default)]
|
||||
pub image_context: ImageContextConfig,
|
||||
#[serde(default)]
|
||||
pub subagents: SubagentsConfig,
|
||||
}
|
||||
|
||||
/// 图片上下文限制配置
|
||||
@ -136,6 +138,34 @@ impl Default for SkillsConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// 自定义子代理配置
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct SubagentsConfig {
|
||||
/// 是否启用自定义子代理发现
|
||||
#[serde(default = "default_subagents_enabled")]
|
||||
pub enabled: bool,
|
||||
/// 定义来源优先级
|
||||
#[serde(default = "default_subagents_sources")]
|
||||
pub sources: Vec<String>,
|
||||
}
|
||||
|
||||
fn default_subagents_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_subagents_sources() -> Vec<String> {
|
||||
vec!["user".to_string(), "project".to_string()]
|
||||
}
|
||||
|
||||
impl Default for SubagentsConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: default_subagents_enabled(),
|
||||
sources: default_subagents_sources(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
pub struct ToolsConfig {
|
||||
#[serde(default)]
|
||||
|
||||
@ -14,7 +14,7 @@ use crate::storage::{
|
||||
};
|
||||
use crate::tools::{
|
||||
DefaultSubAgentRuntime, InMemoryTaskRepository, NoopSessionMessageSender,
|
||||
SessionMessageSender, SubAgentRuntimeConfig, ToolRegistry,
|
||||
SessionMessageSender, SubAgentRuntimeConfig, SubagentCatalog, ToolRegistry,
|
||||
};
|
||||
use crate::tools::task::repository::TaskRepository;
|
||||
|
||||
@ -126,11 +126,13 @@ pub(crate) fn build_session_manager_with_sender(
|
||||
let task_repository = Arc::new(InMemoryTaskRepository::new());
|
||||
let subagent_tools = Arc::new(factory.build_subagent_tools());
|
||||
|
||||
// Create subagent catalog with builtin definitions
|
||||
let catalog = Arc::new(SubagentCatalog::new());
|
||||
|
||||
let runtime_config = SubAgentRuntimeConfig {
|
||||
allowed_tools: task_config.allowed_tools.iter().cloned().collect(),
|
||||
max_execution_secs: task_config.max_execution_secs,
|
||||
default_allowed_tools: task_config.allowed_tools.iter().cloned().collect(),
|
||||
default_max_execution_secs: task_config.max_execution_secs,
|
||||
explore_max_execution_secs: task_config.explore_max_execution_secs,
|
||||
explore_max_tool_calls: 20,
|
||||
ttl_hours: task_config.ttl_hours,
|
||||
skills_index: skills.system_index_prompt(),
|
||||
};
|
||||
@ -141,6 +143,7 @@ pub(crate) fn build_session_manager_with_sender(
|
||||
conversations.clone(),
|
||||
subagent_tools,
|
||||
provider_config.clone(),
|
||||
catalog,
|
||||
));
|
||||
|
||||
(factory.with_subagent_runtime(subagent_runtime), task_repository)
|
||||
|
||||
@ -36,7 +36,7 @@ pub use skill_activate::SkillActivateTool;
|
||||
pub use skill_manage::SkillManageTool;
|
||||
pub use task::{
|
||||
DefaultSubAgentRuntime, InMemoryTaskRepository, SubAgentRuntime, SubAgentRuntimeConfig,
|
||||
TaskError, TaskRepository, TaskTool,
|
||||
SubagentCatalog, TaskError, TaskRepository, TaskTool,
|
||||
};
|
||||
pub use time::TimeTool;
|
||||
pub use traits::{Tool, ToolContext, ToolResult};
|
||||
|
||||
@ -8,6 +8,6 @@ pub mod types;
|
||||
pub use error::TaskError;
|
||||
pub use prompt::SubagentPromptBuilder;
|
||||
pub use repository::{InMemoryTaskRepository, TaskRepository};
|
||||
pub use runtime::{DefaultSubAgentRuntime, SubAgentRuntime, SubAgentRuntimeConfig, StaticSystemPromptProvider};
|
||||
pub use runtime::{DefaultSubAgentRuntime, SubAgentRuntime, SubAgentRuntimeConfig, SubagentCatalog, StaticSystemPromptProvider};
|
||||
pub use tool::TaskTool;
|
||||
pub use types::{SubagentType, TaskDefinition, TaskHandle, TaskSession, TaskSessionState, TaskToolArgs, TaskToolResult};
|
||||
pub use types::{SubagentDef, SubagentSource, SubagentType, TaskDefinition, TaskHandle, TaskSession, TaskSessionState, TaskToolArgs, TaskToolResult};
|
||||
@ -1,4 +1,4 @@
|
||||
use super::types::SubagentType;
|
||||
use super::types::SubagentDef;
|
||||
use crate::config::LLMProviderConfig;
|
||||
|
||||
/// 子代理系统提示词构建器
|
||||
@ -7,16 +7,13 @@ pub struct SubagentPromptBuilder;
|
||||
impl SubagentPromptBuilder {
|
||||
/// 构建子代理系统提示词(包含系统环境信息和技能索引)
|
||||
pub fn build(
|
||||
subagent_type: SubagentType,
|
||||
def: &SubagentDef,
|
||||
description: &str,
|
||||
_prompt: &str,
|
||||
prompt: &str,
|
||||
config: &LLMProviderConfig,
|
||||
skills_index: Option<&str>,
|
||||
) -> String {
|
||||
let base_prompt = match subagent_type {
|
||||
SubagentType::General => Self::build_general_prompt(description),
|
||||
SubagentType::Explore => Self::build_explore_prompt(description),
|
||||
};
|
||||
let base_prompt = Self::interpolate_template(def, description, prompt);
|
||||
let env_info = crate::agent::generate_system_env_prompt(config);
|
||||
|
||||
// 组合提示词:基础 + 环境 + 技能索引(可选)
|
||||
@ -44,32 +41,19 @@ impl SubagentPromptBuilder {
|
||||
)
|
||||
}
|
||||
|
||||
fn build_general_prompt(description: &str) -> String {
|
||||
format!(
|
||||
"你是一个专注的子代理,正在执行一个独立任务。\n\n\
|
||||
任务描述: {}\n\n\
|
||||
你应该:\n\
|
||||
1. 专注于完成任务,不要偏离目标\n\
|
||||
2. 使用可用的工具进行必要操作\n\
|
||||
3. 完成后给出简洁的总结\n\
|
||||
4. 不要尝试创建新的子代理任务\n\n\
|
||||
注意: 你没有访问主对话历史的权限,这是一个独立的执行上下文。",
|
||||
description
|
||||
)
|
||||
}
|
||||
/// 插值提示词模板
|
||||
fn interpolate_template(def: &SubagentDef, description: &str, prompt: &str) -> String {
|
||||
let mut result = def
|
||||
.prompt_template
|
||||
.replace("{{description}}", description)
|
||||
.replace("{{prompt}}", prompt);
|
||||
|
||||
fn build_explore_prompt(description: &str) -> String {
|
||||
format!(
|
||||
"你是一个只读探索代理,用于代码库探索和信息收集。\n\n\
|
||||
任务描述: {}\n\n\
|
||||
你应该:\n\
|
||||
1. 只使用只读工具进行探索\n\
|
||||
2. 专注于理解和收集信息\n\
|
||||
3. 不要进行任何写操作\n\
|
||||
4. 给出简洁的发现总结\n\n\
|
||||
注意: 你是一个只读代理,禁止执行任何修改操作。",
|
||||
description
|
||||
)
|
||||
if let Some(ref body) = def.body {
|
||||
result.push_str("\n\n");
|
||||
result.push_str(body);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,3 +74,87 @@ pub fn extract_summary(content: &str) -> String {
|
||||
first_paragraph
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::tools::task::types::SubagentSource;
|
||||
|
||||
fn test_def() -> SubagentDef {
|
||||
SubagentDef {
|
||||
name: "general".to_string(),
|
||||
description: "测试".to_string(),
|
||||
prompt_template: "任务: {{description}}\n指令: {{prompt}}".to_string(),
|
||||
body: None,
|
||||
allowed_tools: None,
|
||||
max_execution_secs: None,
|
||||
read_only: None,
|
||||
source: SubagentSource::Builtin,
|
||||
path: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_interpolates_template() {
|
||||
let def = test_def();
|
||||
let result = SubagentPromptBuilder::build(
|
||||
&def,
|
||||
"审查代码",
|
||||
"检查安全漏洞",
|
||||
&LLMProviderConfig {
|
||||
provider_type: "openai".to_string(),
|
||||
name: "test".to_string(),
|
||||
base_url: "http://localhost".to_string(),
|
||||
api_key: "test".to_string(),
|
||||
extra_headers: std::collections::HashMap::new(),
|
||||
llm_timeout_secs: 120,
|
||||
memory_maintenance_timeout_secs: 600,
|
||||
model_id: "test".to_string(),
|
||||
temperature: None,
|
||||
max_tokens: None,
|
||||
context_window_tokens: None,
|
||||
model_extra: std::collections::HashMap::new(),
|
||||
max_tool_iterations: 1,
|
||||
tool_result_max_chars: 1000,
|
||||
context_tool_result_trim_chars: 1000,
|
||||
max_images_in_context: 1,
|
||||
max_image_age_rounds: 10,
|
||||
},
|
||||
None,
|
||||
);
|
||||
assert!(result.contains("任务: 审查代码"));
|
||||
assert!(result.contains("指令: 检查安全漏洞"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_appends_body() {
|
||||
let mut def = test_def();
|
||||
def.body = Some("额外指令".to_string());
|
||||
let result = SubagentPromptBuilder::build(
|
||||
&def,
|
||||
"描述",
|
||||
"指令",
|
||||
&LLMProviderConfig {
|
||||
provider_type: "openai".to_string(),
|
||||
name: "test".to_string(),
|
||||
base_url: "http://localhost".to_string(),
|
||||
api_key: "test".to_string(),
|
||||
extra_headers: std::collections::HashMap::new(),
|
||||
llm_timeout_secs: 120,
|
||||
memory_maintenance_timeout_secs: 600,
|
||||
model_id: "test".to_string(),
|
||||
temperature: None,
|
||||
max_tokens: None,
|
||||
context_window_tokens: None,
|
||||
model_extra: std::collections::HashMap::new(),
|
||||
max_tool_iterations: 1,
|
||||
tool_result_max_chars: 1000,
|
||||
context_tool_result_trim_chars: 1000,
|
||||
max_images_in_context: 1,
|
||||
max_image_age_rounds: 10,
|
||||
},
|
||||
None,
|
||||
);
|
||||
assert!(result.contains("额外指令"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,19 +13,17 @@ use crate::tools::{ToolContext, ToolRegistry};
|
||||
use super::error::TaskError;
|
||||
use super::prompt::{extract_summary, SubagentPromptBuilder};
|
||||
use super::repository::TaskRepository;
|
||||
use super::types::{SubagentType, TaskDefinition, TaskSession, TaskToolResult};
|
||||
use super::types::{SubagentDef, TaskDefinition, TaskSession, TaskToolResult};
|
||||
|
||||
/// 子代理运行时配置
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SubAgentRuntimeConfig {
|
||||
/// 子代理可用的工具列表(白名单)
|
||||
pub allowed_tools: HashSet<String>,
|
||||
/// 最大执行时间(秒) - General 类型
|
||||
pub max_execution_secs: u64,
|
||||
/// 默认工具白名单(定义未指定时使用)
|
||||
pub default_allowed_tools: HashSet<String>,
|
||||
/// 默认最大执行时间(秒)
|
||||
pub default_max_execution_secs: u64,
|
||||
/// Explore 类型的最大执行时间(秒)
|
||||
pub explore_max_execution_secs: u64,
|
||||
/// 探索类型的最大工具调用次数
|
||||
pub explore_max_tool_calls: usize,
|
||||
/// 任务 TTL(小时)
|
||||
pub ttl_hours: u64,
|
||||
/// 技能索引(可选,预生成的技能列表字符串)
|
||||
@ -35,7 +33,7 @@ pub struct SubAgentRuntimeConfig {
|
||||
impl Default for SubAgentRuntimeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
allowed_tools: HashSet::from([
|
||||
default_allowed_tools: HashSet::from([
|
||||
"read".to_string(),
|
||||
"edit".to_string(),
|
||||
"write".to_string(),
|
||||
@ -49,9 +47,8 @@ impl Default for SubAgentRuntimeConfig {
|
||||
"skill_list".to_string(),
|
||||
"send_session_message".to_string(), // 用于进度通知
|
||||
]),
|
||||
max_execution_secs: 1200, // 20分钟
|
||||
default_max_execution_secs: 1200, // 20分钟
|
||||
explore_max_execution_secs: 600, // 10分钟
|
||||
explore_max_tool_calls: 20,
|
||||
ttl_hours: 24,
|
||||
skills_index: None,
|
||||
}
|
||||
@ -81,6 +78,9 @@ pub trait SubAgentRuntime: Send + Sync + 'static {
|
||||
|
||||
/// 清理过期任务
|
||||
async fn cleanup_expired(&self) -> Result<usize, TaskError>;
|
||||
|
||||
/// 获取可用的子代理类型列表
|
||||
fn available_subagent_names(&self) -> Vec<String>;
|
||||
}
|
||||
|
||||
/// 静态系统提示词提供者(用于子代理)
|
||||
@ -110,6 +110,8 @@ pub struct DefaultSubAgentRuntime {
|
||||
conversation_repository: Arc<dyn ConversationRepository>,
|
||||
subagent_tools: Arc<ToolRegistry>,
|
||||
provider_config: LLMProviderConfig,
|
||||
/// 子代理定义目录(内置 + 自定义)
|
||||
catalog: Arc<SubagentCatalog>,
|
||||
}
|
||||
|
||||
impl DefaultSubAgentRuntime {
|
||||
@ -119,6 +121,7 @@ impl DefaultSubAgentRuntime {
|
||||
conversation_repository: Arc<dyn ConversationRepository>,
|
||||
subagent_tools: Arc<ToolRegistry>,
|
||||
provider_config: LLMProviderConfig,
|
||||
catalog: Arc<SubagentCatalog>,
|
||||
) -> Self {
|
||||
Self {
|
||||
config,
|
||||
@ -126,9 +129,33 @@ impl DefaultSubAgentRuntime {
|
||||
conversation_repository,
|
||||
subagent_tools,
|
||||
provider_config,
|
||||
catalog,
|
||||
}
|
||||
}
|
||||
|
||||
/// 查找子代理定义,找不到时 fallback 到 general
|
||||
fn find_subagent_def(&self, type_name: &str) -> SubagentDef {
|
||||
self.catalog
|
||||
.find(type_name)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| self.catalog.find("general").expect("general subagent must exist").clone())
|
||||
}
|
||||
|
||||
/// 获取实际使用的工具白名单(预留,未来可用于动态工具过滤)
|
||||
#[allow(dead_code)]
|
||||
fn effective_allowed_tools(&self, def: &SubagentDef) -> HashSet<String> {
|
||||
def.allowed_tools
|
||||
.as_ref()
|
||||
.map(|tools| tools.iter().cloned().collect())
|
||||
.unwrap_or_else(|| self.config.default_allowed_tools.clone())
|
||||
}
|
||||
|
||||
/// 获取实际执行时间
|
||||
fn effective_max_execution_secs(&self, def: &SubagentDef) -> u64 {
|
||||
def.max_execution_secs
|
||||
.unwrap_or(self.config.default_max_execution_secs)
|
||||
}
|
||||
|
||||
/// 创建子代理实例
|
||||
fn create_subagent(
|
||||
&self,
|
||||
@ -163,6 +190,7 @@ impl DefaultSubAgentRuntime {
|
||||
&self,
|
||||
agent: AgentLoop,
|
||||
session: &TaskSession,
|
||||
def: &SubagentDef,
|
||||
prompt: String,
|
||||
) -> Result<TaskToolResult, TaskError> {
|
||||
// 构建初始消息
|
||||
@ -174,10 +202,10 @@ impl DefaultSubAgentRuntime {
|
||||
};
|
||||
|
||||
// 设置超时
|
||||
let max_secs = if session.subagent_type == SubagentType::Explore {
|
||||
let max_secs = if session.subagent_type == "explore" {
|
||||
self.config.explore_max_execution_secs
|
||||
} else {
|
||||
self.config.max_execution_secs
|
||||
self.effective_max_execution_secs(def)
|
||||
};
|
||||
let timeout_duration = Duration::from_secs(max_secs);
|
||||
|
||||
@ -230,7 +258,8 @@ impl DefaultSubAgentRuntime {
|
||||
user_message_count,
|
||||
};
|
||||
|
||||
let timeout_duration = Duration::from_secs(self.config.max_execution_secs);
|
||||
// 使用默认执行时间(恢复任务时原始定义可能已不存在)
|
||||
let timeout_duration = Duration::from_secs(self.config.default_max_execution_secs);
|
||||
|
||||
let result = tokio::time::timeout(
|
||||
timeout_duration,
|
||||
@ -282,7 +311,10 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
|
||||
.clone()
|
||||
.ok_or_else(|| TaskError::MissingContext("channel_name".to_string()))?;
|
||||
|
||||
// 2. 创建任务会话
|
||||
// 2. 查找子代理定义
|
||||
let def = self.find_subagent_def(task.subagent_type.as_str());
|
||||
|
||||
// 3. 创建任务会话
|
||||
let topic_id = parent_context.topic_id.clone();
|
||||
let session = TaskSession::new(
|
||||
session_id,
|
||||
@ -293,7 +325,7 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
|
||||
task.subagent_type,
|
||||
);
|
||||
|
||||
// 3. 在 sessions 表中创建子智能体会话(确保外键约束满足)
|
||||
// 4. 在 sessions 表中创建子智能体会话(确保外键约束满足)
|
||||
let session_title = format!("Subagent: {}", task.description);
|
||||
if let Err(e) = self.conversation_repository.ensure_session(
|
||||
&session.session_id,
|
||||
@ -304,27 +336,27 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
|
||||
tracing::warn!(error = %e, session_id = %session.session_id, "Failed to ensure subagent session");
|
||||
}
|
||||
|
||||
// 4. 保存任务会话
|
||||
// 5. 保存任务会话
|
||||
self.task_repository.save_task_session(&session).await?;
|
||||
|
||||
// 4. 构建子代理系统提示词
|
||||
// 6. 构建子代理系统提示词
|
||||
let system_prompt = SubagentPromptBuilder::build(
|
||||
task.subagent_type,
|
||||
&def,
|
||||
&task.description,
|
||||
&task.prompt,
|
||||
&self.provider_config,
|
||||
self.config.skills_index.as_deref(),
|
||||
);
|
||||
|
||||
// 5. 创建子代理
|
||||
// 7. 创建子代理
|
||||
let agent = self.create_subagent(&session, system_prompt)?;
|
||||
|
||||
// 6. 执行任务
|
||||
// 8. 执行任务
|
||||
let result = self
|
||||
.execute_task(agent, &session, task.prompt.clone())
|
||||
.execute_task(agent, &session, &def, task.prompt.clone())
|
||||
.await;
|
||||
|
||||
// 7. 更新会话状态并保存
|
||||
// 9. 更新会话状态并保存
|
||||
match result {
|
||||
Ok(tool_result) => {
|
||||
let mut session = session;
|
||||
@ -422,4 +454,82 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
|
||||
.await
|
||||
.map_err(TaskError::from)
|
||||
}
|
||||
|
||||
fn available_subagent_names(&self) -> Vec<String> {
|
||||
self.catalog.names()
|
||||
}
|
||||
}
|
||||
|
||||
/// 子代理定义目录
|
||||
///
|
||||
/// 管理所有可用的子代理定义,包括内置和自定义。
|
||||
/// 支持用户级(~/.picobot/subagents/)和项目级(./.picobot/subagents/)定义,
|
||||
/// 项目级定义会覆盖同名的用户级定义。
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SubagentCatalog {
|
||||
definitions: std::collections::HashMap<String, SubagentDef>,
|
||||
}
|
||||
|
||||
impl SubagentCatalog {
|
||||
/// 创建空的目录,并注册内置子代理
|
||||
pub fn new() -> Self {
|
||||
let mut catalog = Self::default();
|
||||
catalog.register(SubagentDef::builtin_general());
|
||||
catalog.register(SubagentDef::builtin_explore());
|
||||
catalog
|
||||
}
|
||||
|
||||
/// 注册一个子代理定义(同名覆盖)
|
||||
pub fn register(&mut self, def: SubagentDef) {
|
||||
self.definitions.insert(def.name.clone(), def);
|
||||
}
|
||||
|
||||
/// 查找子代理定义
|
||||
pub fn find(&self, name: &str) -> Option<&SubagentDef> {
|
||||
self.definitions.get(name)
|
||||
}
|
||||
|
||||
/// 获取所有可用的子代理名称
|
||||
pub fn names(&self) -> Vec<String> {
|
||||
self.definitions.keys().cloned().collect()
|
||||
}
|
||||
|
||||
/// 获取所有可用的子代理定义(用于生成索引提示)
|
||||
pub fn all(&self) -> Vec<&SubagentDef> {
|
||||
self.definitions.values().collect()
|
||||
}
|
||||
|
||||
/// 生成系统索引提示词(用于注入主 agent)
|
||||
pub fn system_index_prompt(&self) -> Option<String> {
|
||||
let defs = self.all();
|
||||
if defs.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut prompt = String::from(
|
||||
"# 子代理系统\n\n\
|
||||
子代理是专用的执行单元,用于处理特定类型的任务。\n\
|
||||
创建子代理任务时,可以选择以下类型之一:\n\n\
|
||||
<available_subagents>\n"
|
||||
);
|
||||
|
||||
for def in defs {
|
||||
prompt.push_str(&format!(
|
||||
" <subagent>\n <name>{}</name>\n <description>{}</description>\n </subagent>\n",
|
||||
xml_escape(&def.name),
|
||||
xml_escape(&def.description),
|
||||
));
|
||||
}
|
||||
|
||||
prompt.push_str("</available_subagents>");
|
||||
Some(prompt)
|
||||
}
|
||||
}
|
||||
|
||||
fn xml_escape(s: &str) -> String {
|
||||
s.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'")
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 子代理会话状态
|
||||
@ -20,34 +22,114 @@ impl Default for TaskSessionState {
|
||||
}
|
||||
}
|
||||
|
||||
/// 子代理类型
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SubagentType {
|
||||
/// 通用型 - 处理复杂多步骤任务
|
||||
#[default]
|
||||
General,
|
||||
/// 探索型 - 只读搜索代理
|
||||
Explore,
|
||||
/// 子代理来源
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SubagentSource {
|
||||
/// 内置定义
|
||||
Builtin,
|
||||
/// 用户级自定义 (~/.picobot/subagents/)
|
||||
User,
|
||||
/// 项目级自定义 (./.picobot/subagents/)
|
||||
Project,
|
||||
}
|
||||
|
||||
impl SubagentType {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::General => "general",
|
||||
Self::Explore => "explore",
|
||||
/// 子代理完整定义
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SubagentDef {
|
||||
/// 名称标识
|
||||
pub name: String,
|
||||
/// 简短描述(用于 agent 选择)
|
||||
pub description: String,
|
||||
/// 提示词模板,支持 {{description}} 和 {{prompt}} 插值
|
||||
pub prompt_template: String,
|
||||
/// 可选的详细指令(body 部分)
|
||||
pub body: Option<String>,
|
||||
/// 工具白名单(None 表示使用默认)
|
||||
pub allowed_tools: Option<Vec<String>>,
|
||||
/// 最大执行时间(秒),None 表示使用默认
|
||||
pub max_execution_secs: Option<u64>,
|
||||
/// 是否只读代理
|
||||
pub read_only: Option<bool>,
|
||||
/// 来源
|
||||
pub source: SubagentSource,
|
||||
/// 文件路径(仅自定义类型)
|
||||
pub path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl SubagentDef {
|
||||
/// 创建内置 general 子代理定义
|
||||
pub fn builtin_general() -> Self {
|
||||
Self {
|
||||
name: "general".to_string(),
|
||||
description: "通用型子代理 - 处理复杂多步骤任务".to_string(),
|
||||
prompt_template: "你是一个专注的子代理,正在执行一个独立任务。\n\n任务描述: {{description}}\n\n你应该:\n1. 专注于完成任务,不要偏离目标\n2. 使用可用的工具进行必要操作\n3. 完成后给出简洁的总结\n4. 不要尝试创建新的子代理任务\n\n注意: 你没有访问主对话历史的权限,这是一个独立的执行上下文。".to_string(),
|
||||
body: None,
|
||||
allowed_tools: None,
|
||||
max_execution_secs: None,
|
||||
read_only: Some(false),
|
||||
source: SubagentSource::Builtin,
|
||||
path: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"general" => Some(Self::General),
|
||||
"explore" => Some(Self::Explore),
|
||||
_ => None,
|
||||
/// 创建内置 explore 子代理定义
|
||||
pub fn builtin_explore() -> Self {
|
||||
Self {
|
||||
name: "explore".to_string(),
|
||||
description: "探索型子代理 - 只读搜索代理".to_string(),
|
||||
prompt_template: "你是一个只读探索代理,用于代码库探索和信息收集。\n\n任务描述: {{description}}\n\n你应该:\n1. 只使用只读工具进行探索\n2. 专注于理解和收集信息\n3. 不要进行任何写操作\n4. 给出简洁的发现总结\n\n注意: 你是一个只读代理,禁止执行任何修改操作。".to_string(),
|
||||
body: None,
|
||||
allowed_tools: None,
|
||||
max_execution_secs: None,
|
||||
read_only: Some(true),
|
||||
source: SubagentSource::Builtin,
|
||||
path: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 子代理类型标识(用于 API 参数)
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct SubagentType {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl SubagentType {
|
||||
pub fn new(name: &str) -> Self {
|
||||
Self {
|
||||
name: if name.is_empty() {
|
||||
"general".to_string()
|
||||
} else {
|
||||
name.to_string()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for SubagentType {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SubagentType {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
Ok(Self::new(&s))
|
||||
}
|
||||
}
|
||||
|
||||
/// 任务会话记录
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TaskSession {
|
||||
@ -65,8 +147,8 @@ pub struct TaskSession {
|
||||
pub parent_channel_name: String,
|
||||
/// 任务描述
|
||||
pub description: String,
|
||||
/// 子代理类型
|
||||
pub subagent_type: SubagentType,
|
||||
/// 子代理类型名称
|
||||
pub subagent_type: String,
|
||||
/// 当前状态
|
||||
pub state: TaskSessionState,
|
||||
/// 创建时间
|
||||
@ -99,7 +181,7 @@ impl TaskSession {
|
||||
parent_chat_id,
|
||||
parent_channel_name,
|
||||
description,
|
||||
subagent_type,
|
||||
subagent_type: subagent_type.name.clone(),
|
||||
state: TaskSessionState::Running,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
@ -137,7 +219,7 @@ pub struct TaskToolArgs {
|
||||
pub description: String,
|
||||
/// 详细指令
|
||||
pub prompt: String,
|
||||
/// 子代理类型
|
||||
/// 子代理类型名称(默认 general)
|
||||
#[serde(default)]
|
||||
pub subagent_type: SubagentType,
|
||||
/// 恢复现有会话的 task_id
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user