use std::collections::BTreeSet;
use super::child_config::ChildConfig;
pub mod tool {
pub const READ_FILE: &str = "read_file";
pub const WRITE_FILE: &str = "write_file";
pub const EDIT_FILE: &str = "edit_file";
pub const LIST_FILES: &str = "list_files";
pub const EXECUTE_COMMAND: &str = "execute_command";
}
fn tools(names: &[&str]) -> BTreeSet<String> {
names.iter().map(|n| (*n).to_string()).collect()
}
pub fn default_write_tools() -> Vec<String> {
vec![
tool::WRITE_FILE.to_string(),
tool::EDIT_FILE.to_string(),
tool::EXECUTE_COMMAND.to_string(),
]
}
#[derive(Debug, Clone)]
pub struct ChildPreset {
pub name: &'static str,
pub description: &'static str,
pub config: ChildConfig,
}
impl ChildPreset {
pub fn researcher() -> Self {
Self {
name: "researcher",
description: "代码研究/分析,只读",
config: ChildConfig {
system_prompt: Some(
"你是一个代码研究专家。分析代码结构、理解逻辑、\
发现模式;只读,把发现写进最终回答。"
.into(),
),
tool_names: Some(tools(&[tool::READ_FILE, tool::LIST_FILES])),
max_turns: Some(32),
..Default::default()
},
}
}
pub fn coder() -> Self {
Self {
name: "coder",
description: "代码编写与修改",
config: ChildConfig {
system_prompt: Some(
"你是一个代码编写专家。根据需求编写高质量代码,\
改动最小化。"
.into(),
),
tool_names: Some(tools(&[
tool::READ_FILE,
tool::WRITE_FILE,
tool::EDIT_FILE,
tool::LIST_FILES,
tool::EXECUTE_COMMAND,
])),
max_turns: Some(64),
..Default::default()
},
}
}
pub fn reviewer() -> Self {
Self {
name: "reviewer",
description: "代码评审,只读",
config: ChildConfig {
system_prompt: Some(
"你是一个代码评审专家。审查正确性、风格与安全问题;\
只读,把评审意见写进最终回答。"
.into(),
),
tool_names: Some(tools(&[tool::READ_FILE, tool::LIST_FILES])),
max_turns: Some(32),
..Default::default()
},
}
}
pub fn tester() -> Self {
Self {
name: "tester",
description: "测试编写与执行",
config: ChildConfig {
system_prompt: Some(
"你是一个测试专家。为代码编写测试并执行,\
报告通过/失败与覆盖率。"
.into(),
),
tool_names: Some(tools(&[
tool::READ_FILE,
tool::WRITE_FILE,
tool::EDIT_FILE,
tool::LIST_FILES,
tool::EXECUTE_COMMAND,
])),
max_turns: Some(64),
..Default::default()
},
}
}
pub fn custom(name: &'static str, description: &'static str, config: ChildConfig) -> Self {
Self {
name,
description,
config,
}
}
pub fn by_name(name: &str) -> Option<Self> {
match name {
"researcher" => Some(Self::researcher()),
"coder" => Some(Self::coder()),
"reviewer" => Some(Self::reviewer()),
"tester" => Some(Self::tester()),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use agent_base::{Content, Tool, ToolContext, llm_trait::LlmProvider};
use super::*;
use crate::multi_agent::child_config::ChildConfig as Cfg;
use crate::multi_agent::config::MultiAgentConfig;
use crate::multi_agent::runtime::MultiAgentRuntime;
struct NamedTool(&'static str);
#[async_trait::async_trait]
impl Tool for NamedTool {
fn name(&self) -> &'static str {
self.0
}
fn description(&self) -> &'static str {
"fixture"
}
fn schema(&self) -> serde_json::Value {
serde_json::json!({ "type": "object", "properties": {} })
}
async fn call(
&self,
_args: &serde_json::Value,
_ctx: &ToolContext,
) -> agent_base::AgentResult<Vec<Content>> {
Ok(vec![Content::text("ok")])
}
}
struct QuietLlm;
#[async_trait::async_trait]
impl LlmProvider for QuietLlm {
async fn stream(
&self,
_request: agent_base::llm_trait::ChatRequest,
) -> Result<agent_base::llm_trait::ChatStream, agent_base::llm_trait::LlmError> {
Ok(agent_base::llm_trait::ChatStream::new(Box::pin(
futures_util::stream::iter(vec![Ok(agent_base::StreamChunk::Stop {
finish_reason: Some("stop".to_string()),
})]),
)))
}
async fn chat(
&self,
_request: agent_base::llm_trait::ChatRequest,
) -> Result<agent_base::llm_trait::ChatResponse, agent_base::llm_trait::LlmError> {
unreachable!("unused")
}
fn capabilities(&self) -> agent_base::llm_trait::Capabilities {
agent_base::llm_trait::Capabilities::default()
}
fn info(&self) -> agent_base::llm_trait::ProviderInfo {
agent_base::llm_trait::ProviderInfo {
name: "quiet".into(),
model: "quiet".into(),
version: None,
}
}
}
fn fixture_runtime() -> Arc<MultiAgentRuntime> {
let business_tools: Vec<Arc<dyn Tool>> = [
tool::READ_FILE,
tool::WRITE_FILE,
tool::EDIT_FILE,
tool::LIST_FILES,
tool::EXECUTE_COMMAND,
]
.into_iter()
.map(|n| Arc::new(NamedTool(n)) as Arc<dyn Tool>)
.collect();
Arc::new(MultiAgentRuntime::new(
MultiAgentConfig::enabled(),
Arc::new(QuietLlm),
business_tools,
tokio_util::sync::CancellationToken::new(),
None,
agent_base::Language::En,
None,
None,
))
}
#[tokio::test(flavor = "multi_thread")]
async fn every_preset_whitelist_resolves_against_fixture() {
let ma = fixture_runtime();
for preset in [
ChildPreset::researcher(),
ChildPreset::coder(),
ChildPreset::reviewer(),
ChildPreset::tester(),
] {
let spawned = ma
.spawn_with_config(format!("p_{}", preset.name), preset.config.clone())
.await
.unwrap_or_else(|e| panic!("preset {} failed to spawn: {e}", preset.name));
assert_eq!(
spawned.spawned_tools(),
preset.config.tool_names.as_ref().unwrap(),
"preset {}",
preset.name
);
ma.close_agent(&spawned.agent_path().to_string())
.ok()
.filter(|r| r.closed)
.unwrap_or_else(|| panic!("child {} should close", preset.name));
}
}
#[test]
fn constants_match_phimint_registered_names() {
assert_eq!(tool::READ_FILE, "read_file");
assert_eq!(tool::WRITE_FILE, "write_file");
assert_eq!(tool::EDIT_FILE, "edit_file");
assert_eq!(tool::LIST_FILES, "list_files");
assert_eq!(tool::EXECUTE_COMMAND, "execute_command");
}
#[test]
fn by_name_resolves_four_and_rejects_rest() {
assert_eq!(
ChildPreset::by_name("researcher").unwrap().name,
"researcher"
);
assert_eq!(ChildPreset::by_name("coder").unwrap().name, "coder");
assert_eq!(ChildPreset::by_name("reviewer").unwrap().name, "reviewer");
assert_eq!(ChildPreset::by_name("tester").unwrap().name, "tester");
assert!(ChildPreset::by_name("translator").is_none());
}
#[test]
fn custom_wraps_config_verbatim() {
let cfg = Cfg {
system_prompt: Some("hi".into()),
max_turns: Some(3),
..Default::default()
};
let p = ChildPreset::custom("mine", "d", cfg.clone());
assert_eq!(p.name, "mine");
assert_eq!(p.config.system_prompt.as_deref(), Some("hi"));
assert_eq!(p.config.max_turns, Some(3));
assert!(p.config.context_window.is_none());
assert!(p.config.full_permission.is_none());
}
}