From ea1338c94fb0d61cf608ffbdb3b92e46a7b432dc Mon Sep 17 00:00:00 2001 From: xiaoski Date: Fri, 29 May 2026 11:22:48 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AD=90agent=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/agent/mod.rs | 5 +- src/agent/sub_agent.rs | 84 +++++++++--------- src/agent/system_prompt.rs | 176 +++++++++++++++++++++++++++++++------ src/session/session.rs | 1 + 4 files changed, 197 insertions(+), 69 deletions(-) diff --git a/src/agent/mod.rs b/src/agent/mod.rs index f4af26b..c2b681a 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -7,4 +7,7 @@ pub mod system_prompt; pub use agent_loop::{AgentLoop, AgentError, AgentProcessResult}; pub use context_compressor::{ContextCompressor, estimate_tokens}; pub use sub_agent::{DelegateContext, ExecutionMode, SubAgentConfig, SubAgentError, SubAgentManager, SubAgentResult, TaskNotification, TaskStatus}; -pub use system_prompt::{build_system_prompt, PromptContext, PromptSection, SystemPromptBuilder}; +pub use system_prompt::{ + build_system_prompt, build_sub_agent_system_prompt, PromptContext, PromptSection, + SystemPromptBuilder, +}; diff --git a/src/agent/sub_agent.rs b/src/agent/sub_agent.rs index 4cd6590..51d5a91 100644 --- a/src/agent/sub_agent.rs +++ b/src/agent/sub_agent.rs @@ -6,11 +6,13 @@ use dashmap::DashMap; use tokio_util::sync::CancellationToken; use uuid::Uuid; +use crate::agent::system_prompt::build_sub_agent_system_prompt; use crate::agent::AgentLoop; use crate::agent::AgentError; use crate::bus::ChatMessage; use crate::config::LLMProviderConfig; use crate::providers::{create_provider, LLMProvider}; +use crate::skills::SkillsLoader; use crate::tools::ToolRegistry; tokio::task_local! { @@ -116,6 +118,7 @@ pub struct SubAgentManager { active_tasks: Arc>, notify_tx: tokio::sync::mpsc::UnboundedSender, max_concurrent_background_tasks: usize, + skills_loader: Option>, } impl SubAgentManager { @@ -125,6 +128,7 @@ impl SubAgentManager { storage: Option>, notify_tx: tokio::sync::mpsc::UnboundedSender, max_concurrent_background_tasks: usize, + skills_loader: Option>, ) -> Self { Self { provider_config, @@ -133,6 +137,7 @@ impl SubAgentManager { active_tasks: Arc::new(DashMap::new()), notify_tx, max_concurrent_background_tasks, + skills_loader, } } @@ -150,46 +155,17 @@ impl SubAgentManager { Arc::new(filtered) } - pub fn build_system_prompt(&self, config: &SubAgentConfig, tools: &ToolRegistry) -> String { - let timeout_human = format_duration(config.timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS)); - let tool_descriptions = tools.describe_for_prompt(); - - let http_only_note = if config.allowed_tools.is_none() - || config.allowed_tools.as_ref().is_some_and(|v| v.iter().any(|t| t == "http_request")) - { - "- When using http_request, only the GET method is permitted. \ - Do NOT use POST, PUT, DELETE, or any other method." - } else { - "" - }; - - format!( - "You are a sub-agent working on a delegated task. Complete the task below \ - and return a single, self-contained result.\n\ - \n\ - ## Task\n\ - {task}\n\ - \n\ - ## Rules\n\ - - Focus ONLY on the task above. Do not explore unrelated topics.\n\ - - Use tools only when necessary for the task.\n\ - - Do NOT use the delegate tool — sub-agent recursion is forbidden.\n\ - - If the task cannot be completed, explain why clearly.\n\ - - Return only the final result. Do not describe your process.\n\ - {http_only}\n\ - - Timeout: {timeout_human}. If approaching the limit, return partial results.\n\ - \n\ - ## Available Tools\n\ - {tool_descriptions}\n\ - \n\ - ## Workspace\n\ - {workspace}", - task = config.prompt, - http_only = http_only_note, - timeout_human = timeout_human, - tool_descriptions = tool_descriptions, - workspace = self.provider_config.workspace_dir.display(), - ) + fn get_skills_prompt(&self, tools: &ToolRegistry) -> Option { + let has_get_skill = tools.iter().iter().any(|(name, _)| name == "get_skill"); + if has_get_skill { + if let Some(ref loader) = self.skills_loader { + let prompt = loader.build_skills_prompt(); + if !prompt.is_empty() { + return Some(prompt); + } + } + } + None } pub fn build_sub_agent( @@ -228,8 +204,20 @@ impl SubAgentManager { ) -> Result { let task_id = generate_task_id(); let tools = self.filter_tools(&config.allowed_tools); - let system_prompt = self.build_system_prompt(&config, &tools); let timeout_secs = config.timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS); + let timeout_human = format_duration(timeout_secs); + let http_get_only = config.allowed_tools.is_none() + || config.allowed_tools.as_ref().is_some_and(|v| v.iter().any(|t| t == "http_request")); + let skills_prompt = self.get_skills_prompt(&tools); + let system_prompt = build_sub_agent_system_prompt( + &config.prompt, + &timeout_human, + &tools, + &self.provider_config.workspace_dir, + &self.provider_config.model_id, + skills_prompt, + http_get_only, + ); let agent = self.build_sub_agent(&config, tools) .map_err(|e| SubAgentError::ProviderCreation(e.to_string()))?; @@ -352,8 +340,20 @@ impl SubAgentManager { } let tools = self.filter_tools(&config.allowed_tools); - let system_prompt = self.build_system_prompt(&config, &tools); let timeout_secs = config.timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS); + let timeout_human = format_duration(timeout_secs); + let http_get_only = config.allowed_tools.is_none() + || config.allowed_tools.as_ref().is_some_and(|v| v.iter().any(|t| t == "http_request")); + let skills_prompt = self.get_skills_prompt(&tools); + let system_prompt = build_sub_agent_system_prompt( + &config.prompt, + &timeout_human, + &tools, + &self.provider_config.workspace_dir, + &self.provider_config.model_id, + skills_prompt, + http_get_only, + ); let provider_config = self.provider_config.clone(); let storage = self.storage.clone(); let notify_tx = self.notify_tx.clone(); diff --git a/src/agent/system_prompt.rs b/src/agent/system_prompt.rs index bdcf90f..b2855c4 100644 --- a/src/agent/system_prompt.rs +++ b/src/agent/system_prompt.rs @@ -56,6 +56,30 @@ impl SystemPromptBuilder { } } + /// Create a builder with sub-agent specific sections. + pub fn with_sub_agent_defaults( + task: &str, + timeout: &str, + skills_prompt: Option, + http_get_only: bool, + ) -> Self { + let mut sections: Vec> = vec![ + Box::new(SubAgentIdentitySection { + task: task.to_string(), + timeout: timeout.to_string(), + }), + Box::new(ToolHonestySection), + Box::new(SafetySection), + Box::new(SubAgentToolsSection { http_get_only }), + Box::new(WorkspaceSection), + Box::new(DateTimeSection), + ]; + if let Some(sp) = skills_prompt { + sections.push(Box::new(SubAgentSkillsSection { skills_prompt: sp })); + } + Self { sections } + } + /// Add a custom section to the builder. pub fn add_section(mut self, section: Box) -> Self { self.sections.push(section); @@ -360,32 +384,105 @@ impl PromptSection for DelegationSection { fn build(&self, _ctx: &PromptContext<'_>) -> String { "## 子 Agent 委托原则\n\n\ -当任务复杂需要拆解时,使用 delegate 工具创建子 Agent:\n\ -\n\ -### 何时委托\n\ -- 多个独立子任务可以并行处理时(使用 mode=\"parallel\")\n\ -- 长时间运行的任务需要后台执行时(使用 mode=\"background\")\n\ -- 需要以不同权限(受限工具集)执行时\n\ -\n\ -### 工具分配原则\n\ -- **最小权限**:只给子 Agent 完成其任务所需的最少工具\n\ -- **只读优先**:如果可以只用 file_read、file_search、web_fetch 完成,不要给写权限(bash、file_write、file_edit)\n\ -- **禁止递归**:永远不要把 delegate 工具分配给子 Agent\n\ -- **明确边界**:每个子 Agent 只负责一个清晰、独立的子任务\n\ -\n\ -### 任务描述\n\ -- 任务 prompt 要清晰、具体、有明确输出要求\n\ -- 如需额外约束,直接写在 prompt 中(例如:\"跳过 .tmp 文件\")\n\ -- 明确说明期望的输出格式\n\ -\n\ -### 并行模式\n\ -- 多个无依赖的子任务使用 mode=\"parallel\",任务定义在 tasks 数组中\n\ -- 并行任务之间不应有数据依赖\n\ -- 并行任务数建议不超过 5 个\n\ -\n\ -### 后台模式\n\ -- 预计执行时间超过 30s 的任务使用 mode=\"background\"\n\ -- 后台任务有全局并发上限,如果失败提示用户稍后重试".to_string() + 当任务复杂需要拆解时,使用 delegate 工具创建子 Agent:\n\ + \n\ + ### 何时委托\n\ + - 多个独立子任务可以并行处理时(使用 mode=\"parallel\")\n\ + - 长时间运行的任务需要后台执行时(使用 mode=\"background\")\n\ + - 需要以不同权限(受限工具集)执行时\n\ + \n\ + ### 工具分配原则\n\ + - **最小权限**:只给子 Agent 完成其任务所需的最少工具\n\ + - **只读优先**:如果可以只用 file_read、file_search、web_fetch 完成,不要给写权限(bash、file_write、file_edit)\n\ + - **禁止递归**:永远不要把 delegate 工具分配给子 Agent\n\ + - **明确边界**:每个子 Agent 只负责一个清晰、独立的子任务\n\ + \n\ + ### Skill 分配原则\n\ + - 如果子任务的领域有对应的 skill,在 allowed_tools 中加入 get_skill\n\ + - 在任务 prompt 中明确告诉子 Agent 使用 get_skill 加载哪个技能\n\ + - 例如:\"使用 get_skill action='get' skill_name='pdf' 加载 PDF 处理技能后完成任务\"\n\ + \n\ + ### 任务描述\n\ + - 任务 prompt 要清晰、具体、有明确输出要求\n\ + - 如需额外约束,直接写在 prompt 中(例如:\"跳过 .tmp 文件\")\n\ + - 明确说明期望的输出格式\n\ + \n\ + ### 并行模式\n\ + - 多个无依赖的子任务使用 mode=\"parallel\",任务定义在 tasks 数组中\n\ + - 并行任务之间不应有数据依赖\n\ + - 并行任务数建议不超过 5 个\n\ + \n\ + ### 后台模式\n\ + - 预计执行时间超过 30s 的任务使用 mode=\"background\"\n\ + - 后台任务有全局并发上限,如果失败提示用户稍后重试".to_string() + } +} + +// === Sub-Agent Prompt Sections === + +/// Sub-agent identity and task instructions. +pub struct SubAgentIdentitySection { + pub task: String, + pub timeout: String, +} + +impl PromptSection for SubAgentIdentitySection { + fn name(&self) -> &str { + "sub_agent_identity" + } + + fn build(&self, _ctx: &PromptContext<'_>) -> String { + format!( + "## 子 Agent\n\n\ + 你是主 Agent 派出的子 Agent,负责完成一个具体任务。你的最终回复将汇报给主 Agent。\n\ + \n\ + ## 任务\n\n\ + {}\n\ + \n\ + ## 规则\n\ + - 只专注于上述任务,不要探索无关话题\n\ + - 只在必要时使用工具\n\ + - 不要使用 delegate 工具(禁止递归委托)\n\ + - 如果任务无法完成,清楚说明原因\n\ + - 只返回最终结果,不要描述过程\n\ + - 超时:{},接近时限时返回部分结果", + self.task, self.timeout, + ) + } +} + +/// Sub-agent available tools description. +pub struct SubAgentToolsSection { + pub http_get_only: bool, +} + +impl PromptSection for SubAgentToolsSection { + fn name(&self) -> &str { + "sub_agent_tools" + } + + fn build(&self, ctx: &PromptContext<'_>) -> String { + let mut s = String::from("## 可用工具\n\n"); + s.push_str(&ctx.tools.describe_for_prompt()); + if self.http_get_only { + s.push_str("\n\n**注意**:使用 http_request 时只允许 GET 方法,禁止 POST、PUT、DELETE 等。"); + } + s + } +} + +/// Sub-agent skills information, injected when get_skill tool is available. +pub struct SubAgentSkillsSection { + pub skills_prompt: String, +} + +impl PromptSection for SubAgentSkillsSection { + fn name(&self) -> &str { + "sub_agent_skills" + } + + fn build(&self, _ctx: &PromptContext<'_>) -> String { + self.skills_prompt.clone() } } @@ -445,6 +542,33 @@ pub fn build_system_prompt( SystemPromptBuilder::with_defaults().build(&ctx) } +/// Build a system prompt for a sub-agent with all relevant operational sections. +pub fn build_sub_agent_system_prompt( + task: &str, + timeout_human: &str, + tools: &ToolRegistry, + workspace_dir: &Path, + model_name: &str, + skills_prompt: Option, + http_get_only: bool, +) -> String { + let ctx = PromptContext { + workspace_dir, + model_name, + tools, + session_id: None, + memory_context: None, + has_compressed_history: false, + }; + SystemPromptBuilder::with_sub_agent_defaults( + task, + timeout_human, + skills_prompt, + http_get_only, + ) + .build(&ctx) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/session/session.rs b/src/session/session.rs index 679fb1c..c2d971a 100644 --- a/src/session/session.rs +++ b/src/session/session.rs @@ -870,6 +870,7 @@ impl SessionManager { Some(storage.clone()), notify_tx, max_concurrent_background_tasks, + Some(skills_loader.clone()), )); tools.register(crate::tools::DelegateTool::new(sub_agent_manager.clone()));