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)
|
- shell - 执行 shell 命令(Windows PowerShell/Cmd)
|
||||||
- http_request - HTTP 请求
|
- http_request - HTTP 请求
|
||||||
- web_fetch - 网页抓取
|
- web_fetch - 网页抓取
|
||||||
- task - 创建和管理子代理
|
- task - 创建和管理子代理,支持内置类型(general/explore)和用户自定义类型
|
||||||
|
|
||||||
注意:bash 和 shell 是同一个工具在不同平台上的名称,运行时自动检测。
|
注意: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 是围绕工具调用构建的。当前默认注册的工具包括:
|
PicoBot 的 Agent 是围绕工具调用构建的。当前默认注册的工具包括:
|
||||||
|
|
||||||
@ -520,7 +617,7 @@ PicoBot 的 Agent 是围绕工具调用构建的。当前默认注册的工具
|
|||||||
- bash / shell:执行 shell 命令(同一工具,Unix 下名称为 bash,Windows 下名称为 shell)
|
- bash / shell:执行 shell 命令(同一工具,Unix 下名称为 bash,Windows 下名称为 shell)
|
||||||
- http_request:发起 HTTP 请求
|
- http_request:发起 HTTP 请求
|
||||||
- web_fetch:抓取网页正文
|
- web_fetch:抓取网页正文
|
||||||
- task:创建和管理子代理
|
- task:创建和管理子代理,支持内置类型(general/explore)和用户自定义类型
|
||||||
|
|
||||||
其中:
|
其中:
|
||||||
|
|
||||||
@ -530,9 +627,9 @@ PicoBot 的 Agent 是围绕工具调用构建的。当前默认注册的工具
|
|||||||
- skill_activate 负责把具体技能正文注入当前任务上下文
|
- skill_activate 负责把具体技能正文注入当前任务上下文
|
||||||
- skill_manage 整合了技能列出与管理功能,支持运行时创建、更新、删除和批量禁用
|
- skill_manage 整合了技能列出与管理功能,支持运行时创建、更新、删除和批量禁用
|
||||||
- bash / shell / http_request / web_fetch 让 Agent 具备更强的外部交互能力(bash 和 shell 是同一工具在不同平台的名称)
|
- 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。
|
PicoBot 支持通过 MCP (Model Context Protocol) 扩展工具能力,可以连接外部 MCP servers 并自动发现其提供的工具。配置格式兼容 Claude Desktop / Cursor。
|
||||||
|
|
||||||
@ -609,18 +706,18 @@ MCP 工具会自动注册到 ToolRegistry,命名格式为 `mcp_{server_key}_{t
|
|||||||
- 通过 Tool trait 适配器接入,无需修改核心代码
|
- 通过 Tool trait 适配器接入,无需修改核心代码
|
||||||
- 连接失败不影响 Gateway 运行
|
- 连接失败不影响 Gateway 运行
|
||||||
|
|
||||||
## 9. 调度器机制
|
## 10. 调度器机制
|
||||||
|
|
||||||
PicoBot 带有一个基于 SQLite 的调度器,而不是纯内存或 JSON 文件驱动的任务系统。
|
PicoBot 带有一个基于 SQLite 的调度器,而不是纯内存或 JSON 文件驱动的任务系统。
|
||||||
|
|
||||||
### 9.1 支持的调度类型
|
### 10.1 支持的调度类型
|
||||||
|
|
||||||
- delay:延迟执行一次
|
- delay:延迟执行一次
|
||||||
- interval:固定间隔执行
|
- interval:固定间隔执行
|
||||||
- at:某个绝对时间执行一次
|
- at:某个绝对时间执行一次
|
||||||
- cron:cron 表达式调度
|
- cron:cron 表达式调度
|
||||||
|
|
||||||
### 9.2 支持的任务类型
|
### 10.2 支持的任务类型
|
||||||
|
|
||||||
- internal_event:内部事件
|
- internal_event:内部事件
|
||||||
- outbound_message:直接向目标通道发消息
|
- outbound_message:直接向目标通道发消息
|
||||||
@ -643,7 +740,7 @@ silent_agent_task 和 agent_task 使用同一套 Agent 执行能力,但路由
|
|||||||
- 执行失败时会向主 chat 发送一条失败通知,便于用户感知异常
|
- 执行失败时会向主 chat 发送一条失败通知,便于用户感知异常
|
||||||
- 后台任务的历史、压缩和会话内上下文会留在独立会话中,不污染主会话
|
- 后台任务的历史、压缩和会话内上下文会留在独立会话中,不污染主会话
|
||||||
|
|
||||||
### 9.3 运行时管理
|
### 10.3 运行时管理
|
||||||
|
|
||||||
通过 scheduler_manage 可以进行:
|
通过 scheduler_manage 可以进行:
|
||||||
|
|
||||||
@ -737,22 +834,22 @@ silent_agent_task 和 agent_task 使用同一套 Agent 执行能力,但路由
|
|||||||
- agent_task:用户需要直接收到结果,例如日报提醒、定时播报、定时外发通知
|
- agent_task:用户需要直接收到结果,例如日报提醒、定时播报、定时外发通知
|
||||||
- silent_agent_task:任务需要长期积累独立上下文或后台整理材料,但不应污染主会话,例如周报草稿整理、周期性资料汇总、后台分析任务
|
- silent_agent_task:任务需要长期积累独立上下文或后台整理材料,但不应污染主会话,例如周报草稿整理、周期性资料汇总、后台分析任务
|
||||||
|
|
||||||
## 10. 渠道与运行方式
|
## 11. 渠道与运行方式
|
||||||
|
|
||||||
### 10.1 当前支持的通道
|
### 11.1 当前支持的通道
|
||||||
|
|
||||||
- WebSocket CLI 客户端
|
- WebSocket CLI 客户端
|
||||||
- 飞书通道
|
- 飞书通道
|
||||||
- 微信通道
|
- 微信通道
|
||||||
|
|
||||||
### 10.2 Gateway 接口
|
### 11.2 Gateway 接口
|
||||||
|
|
||||||
网关当前暴露:
|
网关当前暴露:
|
||||||
|
|
||||||
- /health:健康检查
|
- /health:健康检查
|
||||||
- /ws:CLI 客户端连接入口
|
- /ws:CLI 客户端连接入口
|
||||||
|
|
||||||
### 10.3 CLI 使用方式
|
### 11.3 CLI 使用方式
|
||||||
|
|
||||||
程序提供两个主命令:
|
程序提供两个主命令:
|
||||||
|
|
||||||
@ -779,7 +876,7 @@ CLI 中已实现的交互命令包括:
|
|||||||
- /clear
|
- /clear
|
||||||
- /quit
|
- /quit
|
||||||
|
|
||||||
## 11. 配置说明
|
## 12. 配置说明
|
||||||
|
|
||||||
配置默认从以下位置加载:
|
配置默认从以下位置加载:
|
||||||
|
|
||||||
@ -846,9 +943,9 @@ CLI 中已实现的交互命令包括:
|
|||||||
- tools:工具启用/禁用配置(通过 disabled 列表指定禁用的工具)
|
- tools:工具启用/禁用配置(通过 disabled 列表指定禁用的工具)
|
||||||
- time.timezone:时区,默认应使用 IANA 时区名,例如 Asia/Shanghai
|
- time.timezone:时区,默认应使用 IANA 时区名,例如 Asia/Shanghai
|
||||||
|
|
||||||
## 12. 快速开始
|
## 13. 快速开始
|
||||||
|
|
||||||
### 12.1 准备配置
|
### 13.1 准备配置
|
||||||
|
|
||||||
1. 复制并修改 config.json,或把配置放到 ~/.picobot/config.json
|
1. 复制并修改 config.json,或把配置放到 ~/.picobot/config.json
|
||||||
2. 配置好 Provider 的 base_url、api_key、model_id
|
2. 配置好 Provider 的 base_url、api_key、model_id
|
||||||
@ -894,13 +991,13 @@ CLI 中已实现的交互命令包括:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 12.2 启动网关
|
### 13.2 启动网关
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo run -- gateway
|
cargo run -- gateway
|
||||||
```
|
```
|
||||||
|
|
||||||
### 12.3 启动本地 CLI
|
### 13.3 启动本地 CLI
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo run -- agent
|
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
|
cargo run -- agent --gateway-url ws://127.0.0.1:19876/ws
|
||||||
```
|
```
|
||||||
|
|
||||||
### 12.4 检查服务状态
|
### 13.4 检查服务状态
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl http://127.0.0.1:19876/health
|
curl http://127.0.0.1:19876/health
|
||||||
```
|
```
|
||||||
|
|
||||||
## 13. 目录结构
|
## 14. 目录结构
|
||||||
|
|
||||||
```text
|
```text
|
||||||
PicoBot/
|
PicoBot/
|
||||||
@ -947,7 +1044,7 @@ PicoBot/
|
|||||||
│ ├── skills/ # 技能运行时
|
│ ├── skills/ # 技能运行时
|
||||||
│ ├── storage/ # SQLite 持久化(存储、端口、记录、错误)
|
│ ├── storage/ # SQLite 持久化(存储、端口、记录、错误)
|
||||||
│ ├── text/ # 文本处理工具
|
│ ├── text/ # 文本处理工具
|
||||||
│ └── tools/ # 内置工具集合
|
│ └── tools/ # 内置工具集合(含 task 子代理系统)
|
||||||
├── docs/
|
├── docs/
|
||||||
│ ├── IMPLEMENTATION_LOG.md
|
│ ├── IMPLEMENTATION_LOG.md
|
||||||
│ └── PERSISTENCE.md
|
│ └── PERSISTENCE.md
|
||||||
@ -955,7 +1052,7 @@ PicoBot/
|
|||||||
└── config.json
|
└── config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
## 14. 测试与维护建议
|
## 15. 测试与维护建议
|
||||||
|
|
||||||
当前 tests 目录中已经包含 Provider 集成测试和工具调用相关测试,但部分测试依赖外部 API Key,需要先准备 tests/test.env。
|
当前 tests 目录中已经包含 Provider 集成测试和工具调用相关测试,但部分测试依赖外部 API Key,需要先准备 tests/test.env。
|
||||||
|
|
||||||
@ -969,13 +1066,13 @@ PicoBot/
|
|||||||
- src/bus/message.rs:消息结构变更(如 OutboundMessage 新增 session_id)
|
- src/bus/message.rs:消息结构变更(如 OutboundMessage 新增 session_id)
|
||||||
- src/command/handlers/:命令处理器实现
|
- src/command/handlers/:命令处理器实现
|
||||||
|
|
||||||
## 15. 总结
|
## 16. 总结
|
||||||
|
|
||||||
PicoBot 当前已经具备一个可长期运行 Agent 系统的关键组件:
|
PicoBot 当前已经具备一个可长期运行 Agent 系统的关键组件:
|
||||||
|
|
||||||
- 有入口:Gateway + Channel
|
- 有入口:Gateway + Channel
|
||||||
- 有状态:SQLite + Session 恢复
|
- 有状态:SQLite + Session 恢复
|
||||||
- 有能力:工具调用 + 技能系统 + MCP 扩展
|
- 有能力:工具调用 + 技能系统 + MCP 扩展 + 可自定义子代理
|
||||||
- 有记忆:长期记忆 + 自动维护摘要
|
- 有记忆:长期记忆 + 自动维护摘要
|
||||||
- 有计划:Scheduler + agent_task
|
- 有计划:Scheduler + agent_task
|
||||||
|
|
||||||
|
|||||||
@ -78,6 +78,7 @@ impl InitWizard {
|
|||||||
memory_maintenance: crate::config::MemoryMaintenanceConfig::default(),
|
memory_maintenance: crate::config::MemoryMaintenanceConfig::default(),
|
||||||
mcp_servers: HashMap::new(),
|
mcp_servers: HashMap::new(),
|
||||||
image_context: crate::config::ImageContextConfig::default(),
|
image_context: crate::config::ImageContextConfig::default(),
|
||||||
|
subagents: crate::config::SubagentsConfig::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -830,6 +831,7 @@ impl InitWizard {
|
|||||||
memory_maintenance: existing.memory_maintenance.clone(),
|
memory_maintenance: existing.memory_maintenance.clone(),
|
||||||
mcp_servers: existing.mcp_servers.clone(),
|
mcp_servers: existing.mcp_servers.clone(),
|
||||||
image_context: existing.image_context.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>,
|
pub mcp_servers: HashMap<String, crate::mcp::McpServerConfig>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub image_context: ImageContextConfig,
|
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)]
|
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||||
pub struct ToolsConfig {
|
pub struct ToolsConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|||||||
@ -14,7 +14,7 @@ use crate::storage::{
|
|||||||
};
|
};
|
||||||
use crate::tools::{
|
use crate::tools::{
|
||||||
DefaultSubAgentRuntime, InMemoryTaskRepository, NoopSessionMessageSender,
|
DefaultSubAgentRuntime, InMemoryTaskRepository, NoopSessionMessageSender,
|
||||||
SessionMessageSender, SubAgentRuntimeConfig, ToolRegistry,
|
SessionMessageSender, SubAgentRuntimeConfig, SubagentCatalog, ToolRegistry,
|
||||||
};
|
};
|
||||||
use crate::tools::task::repository::TaskRepository;
|
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 task_repository = Arc::new(InMemoryTaskRepository::new());
|
||||||
let subagent_tools = Arc::new(factory.build_subagent_tools());
|
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 {
|
let runtime_config = SubAgentRuntimeConfig {
|
||||||
allowed_tools: task_config.allowed_tools.iter().cloned().collect(),
|
default_allowed_tools: task_config.allowed_tools.iter().cloned().collect(),
|
||||||
max_execution_secs: task_config.max_execution_secs,
|
default_max_execution_secs: task_config.max_execution_secs,
|
||||||
explore_max_execution_secs: task_config.explore_max_execution_secs,
|
explore_max_execution_secs: task_config.explore_max_execution_secs,
|
||||||
explore_max_tool_calls: 20,
|
|
||||||
ttl_hours: task_config.ttl_hours,
|
ttl_hours: task_config.ttl_hours,
|
||||||
skills_index: skills.system_index_prompt(),
|
skills_index: skills.system_index_prompt(),
|
||||||
};
|
};
|
||||||
@ -141,6 +143,7 @@ pub(crate) fn build_session_manager_with_sender(
|
|||||||
conversations.clone(),
|
conversations.clone(),
|
||||||
subagent_tools,
|
subagent_tools,
|
||||||
provider_config.clone(),
|
provider_config.clone(),
|
||||||
|
catalog,
|
||||||
));
|
));
|
||||||
|
|
||||||
(factory.with_subagent_runtime(subagent_runtime), task_repository)
|
(factory.with_subagent_runtime(subagent_runtime), task_repository)
|
||||||
|
|||||||
@ -36,7 +36,7 @@ pub use skill_activate::SkillActivateTool;
|
|||||||
pub use skill_manage::SkillManageTool;
|
pub use skill_manage::SkillManageTool;
|
||||||
pub use task::{
|
pub use task::{
|
||||||
DefaultSubAgentRuntime, InMemoryTaskRepository, SubAgentRuntime, SubAgentRuntimeConfig,
|
DefaultSubAgentRuntime, InMemoryTaskRepository, SubAgentRuntime, SubAgentRuntimeConfig,
|
||||||
TaskError, TaskRepository, TaskTool,
|
SubagentCatalog, TaskError, TaskRepository, TaskTool,
|
||||||
};
|
};
|
||||||
pub use time::TimeTool;
|
pub use time::TimeTool;
|
||||||
pub use traits::{Tool, ToolContext, ToolResult};
|
pub use traits::{Tool, ToolContext, ToolResult};
|
||||||
|
|||||||
@ -8,6 +8,6 @@ pub mod types;
|
|||||||
pub use error::TaskError;
|
pub use error::TaskError;
|
||||||
pub use prompt::SubagentPromptBuilder;
|
pub use prompt::SubagentPromptBuilder;
|
||||||
pub use repository::{InMemoryTaskRepository, TaskRepository};
|
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 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;
|
use crate::config::LLMProviderConfig;
|
||||||
|
|
||||||
/// 子代理系统提示词构建器
|
/// 子代理系统提示词构建器
|
||||||
@ -7,16 +7,13 @@ pub struct SubagentPromptBuilder;
|
|||||||
impl SubagentPromptBuilder {
|
impl SubagentPromptBuilder {
|
||||||
/// 构建子代理系统提示词(包含系统环境信息和技能索引)
|
/// 构建子代理系统提示词(包含系统环境信息和技能索引)
|
||||||
pub fn build(
|
pub fn build(
|
||||||
subagent_type: SubagentType,
|
def: &SubagentDef,
|
||||||
description: &str,
|
description: &str,
|
||||||
_prompt: &str,
|
prompt: &str,
|
||||||
config: &LLMProviderConfig,
|
config: &LLMProviderConfig,
|
||||||
skills_index: Option<&str>,
|
skills_index: Option<&str>,
|
||||||
) -> String {
|
) -> String {
|
||||||
let base_prompt = match subagent_type {
|
let base_prompt = Self::interpolate_template(def, description, prompt);
|
||||||
SubagentType::General => Self::build_general_prompt(description),
|
|
||||||
SubagentType::Explore => Self::build_explore_prompt(description),
|
|
||||||
};
|
|
||||||
let env_info = crate::agent::generate_system_env_prompt(config);
|
let env_info = crate::agent::generate_system_env_prompt(config);
|
||||||
|
|
||||||
// 组合提示词:基础 + 环境 + 技能索引(可选)
|
// 组合提示词:基础 + 环境 + 技能索引(可选)
|
||||||
@ -44,32 +41,19 @@ impl SubagentPromptBuilder {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_general_prompt(description: &str) -> String {
|
/// 插值提示词模板
|
||||||
format!(
|
fn interpolate_template(def: &SubagentDef, description: &str, prompt: &str) -> String {
|
||||||
"你是一个专注的子代理,正在执行一个独立任务。\n\n\
|
let mut result = def
|
||||||
任务描述: {}\n\n\
|
.prompt_template
|
||||||
你应该:\n\
|
.replace("{{description}}", description)
|
||||||
1. 专注于完成任务,不要偏离目标\n\
|
.replace("{{prompt}}", prompt);
|
||||||
2. 使用可用的工具进行必要操作\n\
|
|
||||||
3. 完成后给出简洁的总结\n\
|
if let Some(ref body) = def.body {
|
||||||
4. 不要尝试创建新的子代理任务\n\n\
|
result.push_str("\n\n");
|
||||||
注意: 你没有访问主对话历史的权限,这是一个独立的执行上下文。",
|
result.push_str(body);
|
||||||
description
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_explore_prompt(description: &str) -> String {
|
result
|
||||||
format!(
|
|
||||||
"你是一个只读探索代理,用于代码库探索和信息收集。\n\n\
|
|
||||||
任务描述: {}\n\n\
|
|
||||||
你应该:\n\
|
|
||||||
1. 只使用只读工具进行探索\n\
|
|
||||||
2. 专注于理解和收集信息\n\
|
|
||||||
3. 不要进行任何写操作\n\
|
|
||||||
4. 给出简洁的发现总结\n\n\
|
|
||||||
注意: 你是一个只读代理,禁止执行任何修改操作。",
|
|
||||||
description
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,3 +74,87 @@ pub fn extract_summary(content: &str) -> String {
|
|||||||
first_paragraph
|
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::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::{SubagentType, TaskDefinition, TaskSession, TaskToolResult};
|
use super::types::{SubagentDef, TaskDefinition, TaskSession, TaskToolResult};
|
||||||
|
|
||||||
/// 子代理运行时配置
|
/// 子代理运行时配置
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SubAgentRuntimeConfig {
|
pub struct SubAgentRuntimeConfig {
|
||||||
/// 子代理可用的工具列表(白名单)
|
/// 默认工具白名单(定义未指定时使用)
|
||||||
pub allowed_tools: HashSet<String>,
|
pub default_allowed_tools: HashSet<String>,
|
||||||
/// 最大执行时间(秒) - General 类型
|
/// 默认最大执行时间(秒)
|
||||||
pub max_execution_secs: u64,
|
pub default_max_execution_secs: u64,
|
||||||
/// Explore 类型的最大执行时间(秒)
|
/// Explore 类型的最大执行时间(秒)
|
||||||
pub explore_max_execution_secs: u64,
|
pub explore_max_execution_secs: u64,
|
||||||
/// 探索类型的最大工具调用次数
|
|
||||||
pub explore_max_tool_calls: usize,
|
|
||||||
/// 任务 TTL(小时)
|
/// 任务 TTL(小时)
|
||||||
pub ttl_hours: u64,
|
pub ttl_hours: u64,
|
||||||
/// 技能索引(可选,预生成的技能列表字符串)
|
/// 技能索引(可选,预生成的技能列表字符串)
|
||||||
@ -35,7 +33,7 @@ pub struct SubAgentRuntimeConfig {
|
|||||||
impl Default for SubAgentRuntimeConfig {
|
impl Default for SubAgentRuntimeConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
allowed_tools: HashSet::from([
|
default_allowed_tools: HashSet::from([
|
||||||
"read".to_string(),
|
"read".to_string(),
|
||||||
"edit".to_string(),
|
"edit".to_string(),
|
||||||
"write".to_string(),
|
"write".to_string(),
|
||||||
@ -49,9 +47,8 @@ impl Default for SubAgentRuntimeConfig {
|
|||||||
"skill_list".to_string(),
|
"skill_list".to_string(),
|
||||||
"send_session_message".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_execution_secs: 600, // 10分钟
|
||||||
explore_max_tool_calls: 20,
|
|
||||||
ttl_hours: 24,
|
ttl_hours: 24,
|
||||||
skills_index: None,
|
skills_index: None,
|
||||||
}
|
}
|
||||||
@ -81,6 +78,9 @@ pub trait SubAgentRuntime: Send + Sync + 'static {
|
|||||||
|
|
||||||
/// 清理过期任务
|
/// 清理过期任务
|
||||||
async fn cleanup_expired(&self) -> Result<usize, TaskError>;
|
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>,
|
conversation_repository: Arc<dyn ConversationRepository>,
|
||||||
subagent_tools: Arc<ToolRegistry>,
|
subagent_tools: Arc<ToolRegistry>,
|
||||||
provider_config: LLMProviderConfig,
|
provider_config: LLMProviderConfig,
|
||||||
|
/// 子代理定义目录(内置 + 自定义)
|
||||||
|
catalog: Arc<SubagentCatalog>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DefaultSubAgentRuntime {
|
impl DefaultSubAgentRuntime {
|
||||||
@ -119,6 +121,7 @@ impl DefaultSubAgentRuntime {
|
|||||||
conversation_repository: Arc<dyn ConversationRepository>,
|
conversation_repository: Arc<dyn ConversationRepository>,
|
||||||
subagent_tools: Arc<ToolRegistry>,
|
subagent_tools: Arc<ToolRegistry>,
|
||||||
provider_config: LLMProviderConfig,
|
provider_config: LLMProviderConfig,
|
||||||
|
catalog: Arc<SubagentCatalog>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
config,
|
config,
|
||||||
@ -126,9 +129,33 @@ impl DefaultSubAgentRuntime {
|
|||||||
conversation_repository,
|
conversation_repository,
|
||||||
subagent_tools,
|
subagent_tools,
|
||||||
provider_config,
|
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(
|
fn create_subagent(
|
||||||
&self,
|
&self,
|
||||||
@ -163,6 +190,7 @@ impl DefaultSubAgentRuntime {
|
|||||||
&self,
|
&self,
|
||||||
agent: AgentLoop,
|
agent: AgentLoop,
|
||||||
session: &TaskSession,
|
session: &TaskSession,
|
||||||
|
def: &SubagentDef,
|
||||||
prompt: String,
|
prompt: String,
|
||||||
) -> Result<TaskToolResult, TaskError> {
|
) -> 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
|
self.config.explore_max_execution_secs
|
||||||
} else {
|
} else {
|
||||||
self.config.max_execution_secs
|
self.effective_max_execution_secs(def)
|
||||||
};
|
};
|
||||||
let timeout_duration = Duration::from_secs(max_secs);
|
let timeout_duration = Duration::from_secs(max_secs);
|
||||||
|
|
||||||
@ -230,7 +258,8 @@ impl DefaultSubAgentRuntime {
|
|||||||
user_message_count,
|
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(
|
let result = tokio::time::timeout(
|
||||||
timeout_duration,
|
timeout_duration,
|
||||||
@ -282,7 +311,10 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
|
|||||||
.clone()
|
.clone()
|
||||||
.ok_or_else(|| TaskError::MissingContext("channel_name".to_string()))?;
|
.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 topic_id = parent_context.topic_id.clone();
|
||||||
let session = TaskSession::new(
|
let session = TaskSession::new(
|
||||||
session_id,
|
session_id,
|
||||||
@ -293,7 +325,7 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
|
|||||||
task.subagent_type,
|
task.subagent_type,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 3. 在 sessions 表中创建子智能体会话(确保外键约束满足)
|
// 4. 在 sessions 表中创建子智能体会话(确保外键约束满足)
|
||||||
let session_title = format!("Subagent: {}", task.description);
|
let session_title = format!("Subagent: {}", task.description);
|
||||||
if let Err(e) = self.conversation_repository.ensure_session(
|
if let Err(e) = self.conversation_repository.ensure_session(
|
||||||
&session.session_id,
|
&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");
|
tracing::warn!(error = %e, session_id = %session.session_id, "Failed to ensure subagent session");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 保存任务会话
|
// 5. 保存任务会话
|
||||||
self.task_repository.save_task_session(&session).await?;
|
self.task_repository.save_task_session(&session).await?;
|
||||||
|
|
||||||
// 4. 构建子代理系统提示词
|
// 6. 构建子代理系统提示词
|
||||||
let system_prompt = SubagentPromptBuilder::build(
|
let system_prompt = SubagentPromptBuilder::build(
|
||||||
task.subagent_type,
|
&def,
|
||||||
&task.description,
|
&task.description,
|
||||||
&task.prompt,
|
&task.prompt,
|
||||||
&self.provider_config,
|
&self.provider_config,
|
||||||
self.config.skills_index.as_deref(),
|
self.config.skills_index.as_deref(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 5. 创建子代理
|
// 7. 创建子代理
|
||||||
let agent = self.create_subagent(&session, system_prompt)?;
|
let agent = self.create_subagent(&session, system_prompt)?;
|
||||||
|
|
||||||
// 6. 执行任务
|
// 8. 执行任务
|
||||||
let result = self
|
let result = self
|
||||||
.execute_task(agent, &session, task.prompt.clone())
|
.execute_task(agent, &session, &def, task.prompt.clone())
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// 7. 更新会话状态并保存
|
// 9. 更新会话状态并保存
|
||||||
match result {
|
match result {
|
||||||
Ok(tool_result) => {
|
Ok(tool_result) => {
|
||||||
let mut session = session;
|
let mut session = session;
|
||||||
@ -422,4 +454,82 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
|
|||||||
.await
|
.await
|
||||||
.map_err(TaskError::from)
|
.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};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// 子代理会话状态
|
/// 子代理会话状态
|
||||||
@ -20,31 +22,111 @@ impl Default for TaskSessionState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 子代理类型
|
/// 子代理来源
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum SubagentType {
|
pub enum SubagentSource {
|
||||||
/// 通用型 - 处理复杂多步骤任务
|
/// 内置定义
|
||||||
#[default]
|
Builtin,
|
||||||
General,
|
/// 用户级自定义 (~/.picobot/subagents/)
|
||||||
/// 探索型 - 只读搜索代理
|
User,
|
||||||
Explore,
|
/// 项目级自定义 (./.picobot/subagents/)
|
||||||
|
Project,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 子代理完整定义
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建内置 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 {
|
impl SubagentType {
|
||||||
pub fn as_str(&self) -> &'static str {
|
pub fn new(name: &str) -> Self {
|
||||||
match self {
|
Self {
|
||||||
Self::General => "general",
|
name: if name.is_empty() {
|
||||||
Self::Explore => "explore",
|
"general".to_string()
|
||||||
|
} else {
|
||||||
|
name.to_string()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_str(s: &str) -> Option<Self> {
|
pub fn as_str(&self) -> &str {
|
||||||
match s {
|
&self.name
|
||||||
"general" => Some(Self::General),
|
|
||||||
"explore" => Some(Self::Explore),
|
|
||||||
_ => None,
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,8 +147,8 @@ pub struct TaskSession {
|
|||||||
pub parent_channel_name: String,
|
pub parent_channel_name: String,
|
||||||
/// 任务描述
|
/// 任务描述
|
||||||
pub description: String,
|
pub description: String,
|
||||||
/// 子代理类型
|
/// 子代理类型名称
|
||||||
pub subagent_type: SubagentType,
|
pub subagent_type: String,
|
||||||
/// 当前状态
|
/// 当前状态
|
||||||
pub state: TaskSessionState,
|
pub state: TaskSessionState,
|
||||||
/// 创建时间
|
/// 创建时间
|
||||||
@ -99,7 +181,7 @@ impl TaskSession {
|
|||||||
parent_chat_id,
|
parent_chat_id,
|
||||||
parent_channel_name,
|
parent_channel_name,
|
||||||
description,
|
description,
|
||||||
subagent_type,
|
subagent_type: subagent_type.name.clone(),
|
||||||
state: TaskSessionState::Running,
|
state: TaskSessionState::Running,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
@ -137,7 +219,7 @@ pub struct TaskToolArgs {
|
|||||||
pub description: String,
|
pub description: String,
|
||||||
/// 详细指令
|
/// 详细指令
|
||||||
pub prompt: String,
|
pub prompt: String,
|
||||||
/// 子代理类型
|
/// 子代理类型名称(默认 general)
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub subagent_type: SubagentType,
|
pub subagent_type: SubagentType,
|
||||||
/// 恢复现有会话的 task_id
|
/// 恢复现有会话的 task_id
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user