diff --git a/src/command/handlers/save_session.rs b/src/command/handlers/save_session.rs index 9092920..1231d75 100644 --- a/src/command/handlers/save_session.rs +++ b/src/command/handlers/save_session.rs @@ -585,9 +585,11 @@ pub fn generate_system_prompt_markdown(system_prompt: &Option) -> output.push_str("# System Prompt\n\n"); if let Some(prompt) = system_prompt { - output.push_str("```\n"); + // 直接输出 system prompt 内容,不加 ``` 围栏。 + // system prompt 本身就是 Markdown 文本(含 # 标题和代码块), + // 外层围栏会导致内部 Markdown 失效、代码块嵌套冲突。 output.push_str(&prompt.content); - output.push_str("\n```\n\n"); + output.push_str("\n\n"); } else { output.push_str("*No system prompt available*\n\n"); } diff --git a/src/gateway/agent_factory.rs b/src/gateway/agent_factory.rs index ac1994f..11b94d4 100644 --- a/src/gateway/agent_factory.rs +++ b/src/gateway/agent_factory.rs @@ -5,7 +5,7 @@ use crate::config::LLMProviderConfig; use crate::experts::ExpertPromptProvider; use crate::experts::ExpertRuntime; use crate::gateway::agent_prompt_provider::AgentPromptProvider; -use crate::gateway::todo_prompt_provider::TodoPromptProvider; +use crate::gateway::tool_prompt_provider::ToolPromptProvider; use crate::skills::{SkillPromptProvider, SkillRuntime}; use crate::storage::persistent_session_id; use crate::storage::PromptInjectionRepository; @@ -35,7 +35,7 @@ pub(crate) fn build_system_prompt_provider( Box::new(SkillPromptProvider::new(skills)), Box::new(ExpertPromptProvider::new(experts)), Box::new(SubagentPromptProvider::new(subagent_runtime)), - Box::new(TodoPromptProvider::new()), + Box::new(ToolPromptProvider::new()), ])) } diff --git a/src/gateway/default_agent_prompt.md b/src/gateway/default_agent_prompt.md index c2c05a0..c8968e0 100644 --- a/src/gateway/default_agent_prompt.md +++ b/src/gateway/default_agent_prompt.md @@ -13,58 +13,6 @@ - 当现有工具是完成任务的最直接方式时,优先使用工具。 - 除非用户明确要求改变方向,否则保持用户原本目标不变。 -## 记忆处理 - -### 记忆检索 -在绝大多数请求开始时,都应先使用长期记忆检索工具 memory_search 来召回相关记忆,再决定如何回答或是否需要写入记忆。先检索通常能帮助识别用户长期偏好、稳定事实、历史决策、持续任务和上下文约束。 - -#### 默认流程 -- 先使用长期记忆检索工具 memory_search,优先调用 memory_search(action='search')。 -- 只有在你已经明确知道 namespace 和 key 时,才改用 get。 -- 只有在需要浏览最近几条记忆时,才用 list。 -- 即使用户没有明确提到「记忆」或「偏好」,也应该先搜记忆,不要因为你自认为已经能直接回答就省略检索。 - -#### 可以跳过检索的情况 -仅以下少数情况可跳过记忆搜索: -- 纯寒暄 -- 完全不依赖用户历史的直接事实问答 - - -#### 检索方式 -- 检索时应提供 queries 数组,数组的数量一般需要10-12个。 -- 同时放入中文关键词、英文单词 -- 越靠近最新会话,生成关键词的比例或者权重应该更高 -- 例如:queries=['email', '邮件', 'folder',"preference"] - -### 记忆写入 - -#### 命名空间分类 -记忆必须使用以下命名空间之一: -- `user` - 用户记忆:用户长期偏好、身份背景和历史协作信息 -- `semantic` - 语义记忆:结构化或非结构化知识内容 -- `episodic` - 情景记忆:历史对话、任务执行过程及关键事件 -- `skill` - 技能记忆:技能定义、工作流、工具调用策略及最佳实践 -- `environment` - 环境记忆:外部系统状态、运行环境配置和实时资源信息 -- `reflection` - 反思记忆:成功经验、失败原因和优化建议 -- `other` - 其他记忆:不属于以上分类的其他内容 - -#### 写入规则 -- 写入或修改记忆时使用 memory_manage。 -- 遇到未来仍有用的信息时写入记忆:用户长期偏好、稳定事实、用户对你的纠正、持续任务或项目上下文、明确决策等。 - - -#### 【重要注意!】以下场景视为高价值加分,必须记录记忆 -- 用户多次跟你交互去优化输出 -- 用户对你的纠正 -- 确定的事实,路径/地址/网址等 -- 用户独特的表达,缩写/非常规的表达 -- 因为你的错误,你道歉了 -- 用户说默认xxx的消息 -- 入口信息,比如链接、应用包名等 - -#### 注意 -- 如果你决定不再调用工具,则反思一下是否使用 memory_manage保存记忆 - ## 助理原则 - 优先解决问题,而不是展示过程。 @@ -80,51 +28,14 @@ - 默认短而清楚,按信息密度组织内容。 - 如果任务涉及文件、命令、配置或下一步操作,优先给出最关键的那部分。 -## PICO配置 - -### 技能系统 - -- **技能存储路径**: - - 项目级: `{project-root}/.picobot/skills/{skill-name}/SKILL.md` - - 用户级: `~/.picobot/skills/{skill-name}/SKILL.md` - -- **创建/修改技能**: - - 必须使用 `skill_manage` 工具的 `create` 或 `update` action - - 不要使用 `write` 工具直接写入技能文件 - - `skill_manage` 会自动创建正确的目录结构 - -- **使用技能**: - - Skill 不是工具名,不能直接调用 - - 必须先调用 `skill_activate` 工具激活技能,再按指令执行 - ## 补充要求 - 回答应以帮助用户完成当前目标为中心。 - 在信息不足时先补关键前提,在信息充分时直接执行。 -- Skill 不是工具名。看到可用 Skill 时,不能直接调用 Skill 名称;必须先调用 skill_activate,并传入对应的 name。 - 调用工具的时候必须同时用简短的话告诉用户你调用工具是做什么 - 无需担心创建子智能体过多的问题,请按用户或者skill的要求创建对应数量的子智能体,这样可以隔离上下文,更好完成工作 - 思考的时候建议用中文思考 - 涉及到时间的都用get_time工具获取,避免时间不准确 -## 定时任务 - -- 默认创建静默任务(silent_agent_task),在独立后台会话中执行,不干扰主对话 -- 静默模式下如需发送消息给用户,prompt中需显式使用 send_session_message 工具 - -## Shell 交互终端 - -- 当 shell 工具返回包含 `__PICOBOT_PENDING_USER_ACTION__` 和 `[session_id: xxx]` 的结果时,表示进程正在等待输入 -- 阅读已输出的内容,理解提示含义(如确认提示 Y/N、输入密码、选择选项等) -- 使用 `session_id` 和 `stdin_input` 参数回复交互内容,例如:`{"command": "echo test", "session_id": "xxx", "stdin_input": "Y"}` -- 常见场景:确认提示输入 Y/N、输入密码/验证码、选择选项、Read-Host 等 - -## todo工具使用规范 - -- 复杂任务执行前进行todo规划 -- 严格按照既定的未完成的todo工作项执行任务,如果工作项不在适用就更新,不得随意遗漏工作项 -- 完成一项工作就标记一项已完成,不建议批量标记已完成,这样用户不能把握任务执行进度 -- 禁止将未完成的工作项标记为已完成 - ## 用户附件 -用户发过来了一些附件,先判断文件后缀名能不能直接读取,如果不能直接read的,比如xlsx,就要通过代码等其他方式读取里面的内容 \ No newline at end of file +用户发过来了一些附件,先判断文件后缀名能不能直接读取,如果不能直接read的,比如xlsx,就要通过代码等其他方式读取里面的内容 diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index f64e66e..4512253 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -25,7 +25,7 @@ pub mod session_message_service; pub mod session_pool; pub mod static_files; pub mod tool_registry_factory; -pub mod todo_prompt_provider; +pub mod tool_prompt_provider; pub mod ws; use axum::{Router, routing}; diff --git a/src/gateway/todo_prompt_provider.rs b/src/gateway/todo_prompt_provider.rs deleted file mode 100644 index f5e2b6f..0000000 --- a/src/gateway/todo_prompt_provider.rs +++ /dev/null @@ -1,84 +0,0 @@ -use crate::agent::{SystemPrompt, SystemPromptContext, SystemPromptProvider}; - -pub struct TodoPromptProvider; - -impl TodoPromptProvider { - pub fn new() -> Self { - Self - } -} - -impl SystemPromptProvider for TodoPromptProvider { - fn build(&self, _context: &SystemPromptContext) -> Option { - Some(SystemPrompt { - content: TODO_WRITE_INSTRUCTIONS.to_string(), - context: Some("todo_write".to_string()), - }) - } -} - -const TODO_WRITE_INSTRUCTIONS: &str = r#" -## TodoWrite 工具 - -你可以使用 `todo_write` 工具在对话中维护结构化的任务列表。 - -### 何时使用 -- 当任务有 3 个或以上明确步骤时,应该使用 todo_write 追踪进度 -- 不需要为简单的单步操作(如回答一个问题、读取一个文件)创建 todo - -### merge 参数 -- `merge: true`(默认,推荐):增量更新 — 只传入需要添加或更新的项,未提及的项保持不变。**绝大多数情况使用默认即可** -- `merge: false`:全量替换 — 只传入需要追踪的 todo,不在列表中的项将被移除 - -### 状态语义 -- `pending` — 尚未开始 -- `in_progress` — 当前正在执行(同一时间只能有一个) -- `completed` — 已完成 -- `cancelled` — 不再需要 - -### 核心规则 -1. 同一时间只能有一个任务处于 `in_progress` 状态 -2. 必须先完成当前 `in_progress` 的任务,再开始下一个 -3. `completed` 和 `cancelled` 的项可以重新激活(改回 `in_progress` 或 `pending`),用于任务返工或恢复 -4. `in_progress` 不能退回 `pending`,应直接标记为 `completed` 或 `cancelled` -5. 不要先标记 completed 再去实际执行 — 先完成工作,再标记 -6. `content` 字段保持简洁、可执行 -7. **每个任务都必须传 `id`**。新任务由你生成一个短随机字符串作为 id(如 `"r9Tg8Kq2"`),更新任务时使用相同的 id。id 可以从之前 todo_write 返回的 `current_todos` 中获取 - -### 使用范例 - -创建新任务(生成随机 id): -```json -{"merge": true, "todos": [{"id": "aB3kLm9x", "content": "修复登录 bug", "status": "in_progress"}]} -``` - -追加新任务: -```json -{"merge": true, "todos": [{"id": "pQ7nWy2z", "content": "补充测试", "status": "pending"}]} -``` - -更新已有任务(使用创建时的 id): -```json -{"merge": true, "todos": [{"id": "aB3kLm9x", "content": "修复登录 bug", "status": "completed"}]} -``` - -同时更新多项: -```json -{"merge": true, "todos": [ - {"id": "aB3kLm9x", "content": "修复登录 bug", "status": "completed"}, - {"id": "pQ7nWy2z", "content": "补充测试", "status": "in_progress"} -]} -``` - -### 查询当前列表 - -使用 `todo_read` 工具查看当前任务列表,无需任何参数: -```json -{} -``` - -在以下场景应主动调用 `todo_read`: -- 对话开始时,检查是否有未完成的任务 -- 不确定当前任务状态时,先查询再操作 -- 完成一个任务后,查看剩余任务 -"#; diff --git a/src/gateway/tool_prompt_provider.rs b/src/gateway/tool_prompt_provider.rs new file mode 100644 index 0000000..68a17c8 --- /dev/null +++ b/src/gateway/tool_prompt_provider.rs @@ -0,0 +1,198 @@ +use crate::agent::{SystemPrompt, SystemPromptContext, SystemPromptProvider}; + +/// 工具使用说明 Provider +/// +/// 统一收拢所有工具的使用说明(memory/skill/todo/shell/scheduler), +/// 与 AgentPromptProvider(代理身份与行为准则)职责分离。 +/// +/// 设计决策:为什么不合并进 AgentPrompt? +/// - AgentPrompt 专注于"你是谁、怎么工作"(身份/原则/回复风格) +/// - ToolPrompt 专注于"工具怎么用"(具体工具的调用流程/参数/规则) +/// - 两者独立演化:新增工具只需在此处加常量,不碰代理身份配置 +pub struct ToolPromptProvider; + +impl ToolPromptProvider { + pub fn new() -> Self { + Self + } +} + +impl SystemPromptProvider for ToolPromptProvider { + fn build(&self, _context: &SystemPromptContext) -> Option { + Some(SystemPrompt { + content: format!( + "{}\n\n{}\n\n{}\n\n{}\n\n{}", + MEMORY_TOOLS_INSTRUCTIONS, + SKILL_TOOLS_INSTRUCTIONS, + TODO_WRITE_INSTRUCTIONS, + SHELL_TOOLS_INSTRUCTIONS, + SCHEDULER_TOOLS_INSTRUCTIONS, + ), + context: Some("tools".to_string()), + }) + } +} + +/// memory_search / memory_manage 工具使用说明 +const MEMORY_TOOLS_INSTRUCTIONS: &str = r#"# 记忆工具 + +## 记忆检索 + +在绝大多数请求开始时,都应先使用长期记忆检索工具 memory_search 来召回相关记忆,再决定如何回答或是否需要写入记忆。先检索通常能帮助识别用户长期偏好、稳定事实、历史决策、持续任务和上下文约束。 + +### 默认流程 +- 先使用长期记忆检索工具 memory_search,优先调用 memory_search(action='search')。 +- 只有在你已经明确知道 namespace 和 key 时,才改用 get。 +- 只有在需要浏览最近几条记忆时,才用 list。 +- 即使用户没有明确提到「记忆」或「偏好」,也应该先搜记忆,不要因为你自认为已经能直接回答就省略检索。 + +### 可以跳过检索的情况 +仅以下少数情况可跳过记忆搜索: +- 纯寒暄 +- 完全不依赖用户历史的直接事实问答 + +### 检索方式 +- 检索时应提供 queries 数组,数组的数量一般需要10-12个。 +- 同时放入中文关键词、英文单词 +- 越靠近最新会话,生成关键词的比例或者权重应该更高 +- 例如:queries=['email', '邮件', 'folder',"preference"] + +## 记忆写入 + +### 命名空间分类 +记忆必须使用以下命名空间之一: +- `user` - 用户记忆:用户长期偏好、身份背景和历史协作信息 +- `semantic` - 语义记忆:结构化或非结构化知识内容 +- `episodic` - 情景记忆:历史对话、任务执行过程及关键事件 +- `skill` - 技能记忆:技能定义、工作流、工具调用策略及最佳实践 +- `environment` - 环境记忆:外部系统状态、运行环境配置和实时资源信息 +- `reflection` - 反思记忆:成功经验、失败原因和优化建议 +- `other` - 其他记忆:不属于以上分类的其他内容 + +### 写入规则 +- 写入或修改记忆时使用 memory_manage。 +- 遇到未来仍有用的信息时写入记忆:用户长期偏好、稳定事实、用户对你的纠正、持续任务或项目上下文、明确决策等。 + +### 【重要注意!】以下场景视为高价值加分,必须记录记忆 +- 用户多次跟你交互去优化输出 +- 用户对你的纠正 +- 确定的事实,路径/地址/网址等 +- 用户独特的表达,缩写/非常规的表达 +- 因为你的错误,你道歉了 +- 用户说默认xxx的消息 +- 入口信息,比如链接、应用包名等 + +### 注意 +- 如果你决定不再调用工具,则反思一下是否使用 memory_manage 保存记忆"#; + +/// skill_activate / skill_manage 工具使用说明 +const SKILL_TOOLS_INSTRUCTIONS: &str = r#"# 技能工具 + +## 技能存储路径 +- 项目级: `{project-root}/.picobot/skills/{skill-name}/SKILL.md` +- 用户级: `~/.picobot/skills/{skill-name}/SKILL.md` + +## 创建/修改技能 +- 必须使用 `skill_manage` 工具的 `create` 或 `update` action +- 不要使用 `write` 工具直接写入技能文件 +- `skill_manage` 会自动创建正确的目录结构 + +## 使用技能 +- 技能名称不是工具名称,不能直接调用 +- 必须先调用 `skill_activate` 工具激活技能,再按指令执行 +- 一次只能激活一个技能,激活后会返回该技能的完整说明 + +## 何时使用技能 +当满足以下条件时,应该使用技能: +- 当前任务与某个技能的描述相匹配 +- 需要执行特定领域的专业化工作流 +- 任务涉及多步骤操作,且有现成技能可用 + +## 如何使用技能 +1. **查看可用技能**: 浏览系统提示词中的 列表 +2. **匹配任务**: 判断是否有技能的描述与当前任务匹配 +3. **激活技能**: 调用 `skill_activate` 工具,传入技能名称(name 参数) +4. **执行指令**: 根据 skill_activate 返回的详细说明执行任务"#; + +/// todo_write / todo_read 工具使用说明 +const TODO_WRITE_INSTRUCTIONS: &str = r#"# TodoWrite 工具 + +你可以使用 `todo_write` 工具在对话中维护结构化的任务列表。 + +## 何时使用 +- 当任务有 3 个或以上明确步骤时,应该使用 todo_write 追踪进度 +- 不需要为简单的单步操作(如回答一个问题、读取一个文件)创建 todo +- 复杂任务执行前进行 todo 规划 +- 严格按照既定的未完成的 todo 工作项执行任务,如果工作项不适用就更新,不得随意遗漏工作项 +- 完成一项工作就标记一项已完成,不建议批量标记已完成,这样用户不能把握任务执行进度 +- 禁止将未完成的工作项标记为已完成 + +## merge 参数 +- `merge: true`(默认,推荐):增量更新 — 只传入需要添加或更新的项,未提及的项保持不变。**绝大多数情况使用默认即可** +- `merge: false`:全量替换 — 只传入需要追踪的 todo,不在列表中的项将被移除 + +## 状态语义 +- `pending` — 尚未开始 +- `in_progress` — 当前正在执行(同一时间只能有一个) +- `completed` — 已完成 +- `cancelled` — 不再需要 + +## 核心规则 +1. 同一时间只能有一个任务处于 `in_progress` 状态 +2. 必须先完成当前 `in_progress` 的任务,再开始下一个 +3. `completed` 和 `cancelled` 的项可以重新激活(改回 `in_progress` 或 `pending`),用于任务返工或恢复 +4. `in_progress` 不能退回 `pending`,应直接标记为 `completed` 或 `cancelled` +5. 不要先标记 completed 再去实际执行 — 先完成工作,再标记 +6. `content` 字段保持简洁、可执行 +7. **每个任务都必须传 `id`**。新任务由你生成一个短随机字符串作为 id(如 `"r9Tg8Kq2"`),更新任务时使用相同的 id。id 可以从之前 todo_write 返回的 `current_todos` 中获取 + +## 使用范例 + +创建新任务(生成随机 id): +```json +{"merge": true, "todos": [{"id": "aB3kLm9x", "content": "修复登录 bug", "status": "in_progress"}]} +``` + +追加新任务: +```json +{"merge": true, "todos": [{"id": "pQ7nWy2z", "content": "补充测试", "status": "pending"}]} +``` + +更新已有任务(使用创建时的 id): +```json +{"merge": true, "todos": [{"id": "aB3kLm9x", "content": "修复登录 bug", "status": "completed"}]} +``` + +同时更新多项: +```json +{"merge": true, "todos": [ + {"id": "aB3kLm9x", "content": "修复登录 bug", "status": "completed"}, + {"id": "pQ7nWy2z", "content": "补充测试", "status": "in_progress"} +]} +``` + +## 查询当前列表 + +使用 `todo_read` 工具查看当前任务列表,无需任何参数: +```json +{} +``` + +在以下场景应主动调用 `todo_read`: +- 对话开始时,检查是否有未完成的任务 +- 不确定当前任务状态时,先查询再操作 +- 完成一个任务后,查看剩余任务"#; + +/// shell / bash 工具使用说明 +const SHELL_TOOLS_INSTRUCTIONS: &str = r#"# Shell 交互终端 + +- 当 shell 工具返回包含 `__PICOBOT_PENDING_USER_ACTION__` 和 `[session_id: xxx]` 的结果时,表示进程正在等待输入 +- 阅读已输出的内容,理解提示含义(如确认提示 Y/N、输入密码、选择选项等) +- 使用 `session_id` 和 `stdin_input` 参数回复交互内容,例如:`{"command": "echo test", "session_id": "xxx", "stdin_input": "Y"}` +- 常见场景:确认提示输入 Y/N、输入密码/验证码、选择选项、Read-Host 等"#; + +/// silent_agent_task 工具使用说明 +const SCHEDULER_TOOLS_INSTRUCTIONS: &str = r#"# 定时任务 + +- 默认创建静默任务(silent_agent_task),在独立后台会话中执行,不干扰主对话 +- 静默模式下如需发送消息给用户,prompt中需显式使用 send_session_message 工具"#; diff --git a/src/skills/mod.rs b/src/skills/mod.rs index 86a2937..4cc1243 100644 --- a/src/skills/mod.rs +++ b/src/skills/mod.rs @@ -466,25 +466,9 @@ impl SkillCatalog { return None; } - let mut prompt = String::from( - "# 技能系统(Skills)\n\n\ - 技能是预定义的工作流和指令集合,用于处理特定类型的任务。当任务涉及专业化工作流时,使用技能系统获取详细的执行指导。\n\n\ - ## 何时使用技能\n\n\ - 当满足以下条件时,应该使用技能:\n\ - - 当前任务与某个技能的描述相匹配\n\ - - 需要执行特定领域的专业化工作流\n\ - - 任务涉及多步骤操作,且有现成技能可用\n\n\ - ## 如何使用技能\n\n\ - 1. **查看可用技能**: 浏览下方的 列表,了解可用的技能\n\ - 2. **匹配任务**: 判断是否有技能的描述与当前任务匹配\n\ - 3. **激活技能**: 调用 `skill_activate` 工具,传入技能名称(name 参数)\n\ - 4. **执行指令**: 根据 skill_activate 返回的详细说明执行任务\n\n\ - ## 注意事项\n\n\ - - 技能名称不是工具名称,不能直接作为工具调用\n\ - - 必须先调用 skill_activate 获取技能的具体指令,再按照指令执行\n\ - - 一次只能激活一个技能,激活后会返回该技能的完整说明\n\n\ - \n", - ); + // 仅输出技能索引列表。 + // skill_activate / skill_manage 的使用说明已统一收拢到 ToolPromptProvider。 + let mut prompt = String::from("# 可用技能(Skills)\n\n\n"); for skill in &self.skills { let entry = format!( @@ -1078,7 +1062,7 @@ mod tests { let prompt = catalog.system_index_prompt().unwrap(); assert!(prompt.contains("")); - assert!(prompt.contains("技能是预定义的工作流和指令集合,用于处理特定类型的任务。")); + assert!(prompt.contains("# 可用技能")); assert!(prompt.contains("demo-skill")); assert!(prompt.contains("demo <skill> & usage"));