use crate::command::chat::teammate::{TeammateHandle, TeammateManager, set_current_agent_name};
use crate::command::chat::tools::agent_shared::AgentToolShared;
use crate::command::chat::tools::send_message::SendMessageTool;
use crate::command::chat::tools::{
PlanDecision, Tool, ToolResult, parse_tool_args, schema_to_tool_params,
};
use crate::util::log::write_info_log;
use crate::util::safe_lock;
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::Value;
use std::sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
};
use tokio_util::sync::CancellationToken;
#[derive(Deserialize, JsonSchema)]
struct CreateTeammateParams {
name: String,
role: String,
prompt: String,
#[serde(default)]
worktree: bool,
#[serde(default)]
inherit_permissions: bool,
}
#[allow(dead_code)]
pub struct CreateTeammateTool {
pub shared: AgentToolShared,
pub teammate_manager: Arc<Mutex<TeammateManager>>,
}
impl CreateTeammateTool {
pub const NAME: &'static str = "CreateTeammate";
}
impl Tool for CreateTeammateTool {
fn name(&self) -> &str {
Self::NAME
}
fn description(&self) -> &str {
r#"
Create a new teammate agent that runs independently in the chatroom.
Each teammate has its own LLM connection and conversation context.
Teammates communicate via SendMessage tool (broadcast with @mentions).
Usage:
- name: Short name for the teammate (e.g. "Frontend", "Backend")
- role: Role description (shown in team summary)
- prompt: Initial task/instructions for this teammate
The teammate starts working immediately on the given prompt.
It can use all tools except CreateTeammate (no recursive spawning).
Teammates are session-scoped and cleaned up when the session ends.
Example:
{
"name": "Frontend",
"role": "React TypeScript developer",
"prompt": "Create a React Todo app with components in src/components/..."
}
"#
}
fn parameters_schema(&self) -> Value {
schema_to_tool_params::<CreateTeammateParams>()
}
fn execute(&self, arguments: &str, _cancelled: &Arc<AtomicBool>) -> ToolResult {
let params: CreateTeammateParams = match parse_tool_args(arguments) {
Ok(p) => p,
Err(e) => return e,
};
if params.name.trim().is_empty() {
return ToolResult {
output: "Teammate name cannot be empty".to_string(),
is_error: true,
images: vec![],
plan_decision: PlanDecision::None,
};
}
{
let manager = match self.teammate_manager.lock() {
Ok(m) => m,
Err(_) => {
return ToolResult {
output: "Failed to acquire teammate manager lock".to_string(),
is_error: true,
images: vec![],
plan_decision: PlanDecision::None,
};
}
};
if manager.teammates.contains_key(¶ms.name) {
return ToolResult {
output: format!("Teammate '{}' already exists", params.name),
is_error: true,
images: vec![],
plan_decision: PlanDecision::None,
};
}
}
let worktree_info: Option<(std::path::PathBuf, String)> = if params.worktree {
match crate::command::chat::tools::worktree::create_agent_worktree(¶ms.name) {
Ok(info) => {
write_info_log(
"CreateTeammate",
&format!(
"Teammate '{}' worktree created: {}",
params.name,
info.0.display()
),
);
Some(info)
}
Err(e) => {
return ToolResult {
output: format!("创建 worktree 失败: {}", e),
is_error: true,
images: vec![],
plan_decision: PlanDecision::None,
};
}
}
} else {
None
};
let pending_user_messages = Arc::new(Mutex::new(Vec::new()));
let streaming_content = Arc::new(Mutex::new(String::new()));
let cancel_token = CancellationToken::new();
let is_running = Arc::new(AtomicBool::new(true));
let system_prompt_snapshot = Arc::new(Mutex::new(String::new()));
let messages_snapshot = Arc::new(Mutex::new(Vec::new()));
let provider = safe_lock(&self.shared.provider, "CreateTeammate::provider").clone();
let system_prompt =
safe_lock(&self.shared.system_prompt, "CreateTeammate::system_prompt").clone();
let (mut sub_registry, _) = self.shared.build_sub_registry();
sub_registry.register(Box::new(SendMessageTool {
teammate_manager: Arc::clone(&self.teammate_manager),
}));
let sub_registry = Arc::new(sub_registry);
let mut disabled = self.shared.disabled_tools.as_ref().clone();
disabled.push("CreateTeammate".to_string());
disabled.push("AgentTeam".to_string());
disabled.push("Agent".to_string());
let tools = sub_registry.to_openai_tools_filtered(&disabled);
let jcli_config = if params.inherit_permissions {
let mut cfg = self.shared.jcli_config.as_ref().clone();
cfg.permissions.allow_all = true;
Arc::new(cfg)
} else {
Arc::clone(&self.shared.jcli_config)
};
let teammate_manager = Arc::clone(&self.teammate_manager);
let teammate_name = params.name.clone();
let teammate_role = params.role.clone();
let initial_prompt = params.prompt.clone();
let pending_clone = Arc::clone(&pending_user_messages);
let is_running_clone = Arc::clone(&is_running);
let cancel_token_clone = cancel_token.clone();
let sp_snapshot_clone = Arc::clone(&system_prompt_snapshot);
let msgs_snapshot_clone = Arc::clone(&messages_snapshot);
let thread_handle = std::thread::spawn(move || {
set_current_agent_name(&teammate_name);
if let Some((ref wt_path, _)) = worktree_info {
crate::command::chat::teammate::set_thread_cwd(wt_path);
write_info_log(
"CreateTeammate",
&format!(
"Teammate '{}' working in worktree: {}",
teammate_name,
wt_path.display()
),
);
}
write_info_log(
"CreateTeammate",
&format!("Teammate '{}' agent loop starting", teammate_name),
);
let result = crate::command::chat::teammate_loop::run_teammate_loop(
crate::command::chat::teammate_loop::TeammateLoopConfig {
name: teammate_name.clone(),
role: teammate_role,
initial_prompt,
provider,
base_system_prompt: system_prompt,
tools,
registry: sub_registry,
jcli_config,
teammate_manager,
pending_user_messages: pending_clone,
cancel_token: cancel_token_clone,
system_prompt_snapshot: sp_snapshot_clone,
messages_snapshot: msgs_snapshot_clone,
},
);
if let Some((ref wt_path, ref branch)) = worktree_info {
write_info_log(
"CreateTeammate",
&format!("Teammate '{}' cleaning up worktree", teammate_name),
);
crate::command::chat::tools::worktree::remove_agent_worktree(wt_path, branch);
}
is_running_clone.store(false, Ordering::Relaxed);
write_info_log(
"CreateTeammate",
&format!(
"Teammate '{}' agent loop ended: {}",
teammate_name,
&result[..{
let mut b = result.len().min(200);
while b > 0 && !result.is_char_boundary(b) {
b -= 1;
}
b
}]
),
);
});
let handle = TeammateHandle {
name: params.name.clone(),
role: params.role.clone(),
pending_user_messages,
streaming_content,
cancel_token,
is_running,
thread_handle: Some(thread_handle),
system_prompt_snapshot,
messages_snapshot,
};
match self.teammate_manager.lock() {
Ok(mut manager) => manager.register_teammate(handle),
Err(_) => {
return ToolResult {
output: "Failed to register teammate".to_string(),
is_error: true,
images: vec![],
plan_decision: PlanDecision::None,
};
}
}
let worktree_note = if params.worktree {
" [worktree 隔离]"
} else {
""
};
ToolResult {
output: format!(
"Teammate '{}' ({}){} created and started working on: {}",
params.name,
params.role,
worktree_note,
¶ms.prompt[..{
let mut b = params.prompt.len().min(100);
while b > 0 && !params.prompt.is_char_boundary(b) {
b -= 1;
}
b
}]
),
is_error: false,
images: vec![],
plan_decision: PlanDecision::None,
}
}
fn requires_confirmation(&self) -> bool {
false
}
}