1use crate::agent::thread_identity::{
2 clear_thread_cwd, current_agent_name, set_current_agent_name, set_current_agent_type,
3 set_thread_cwd, thread_cwd,
4};
5use crate::llm::ToolDefinition;
6use crate::permission::JcliConfig;
7use crate::permission::queue::AgentType;
8use crate::storage::{
9 ChatMessage, DisplayHint, MessageRole, ModelProvider, SessionEvent, SessionPaths, ToolCallItem,
10 append_event_to_path,
11};
12use crate::tools::derived_shared::{
13 DerivedAgentShared, LlmNonStreamRequest, SubAgentHandle, SubAgentStatus, ToolExecContext,
14 call_llm_non_stream, create_runtime_and_client, execute_tool_with_permission,
15 extract_tool_items,
16};
17use crate::tools::worktree::{create_agent_worktree, remove_agent_worktree};
18use crate::tools::{
19 PlanDecision, Tool, ToolRegistry, ToolResult, parse_tool_args, schema_to_tool_params,
20};
21use crate::util::log::write_info_log;
22use crate::util::safe_lock;
23use schemars::JsonSchema;
24use serde::Deserialize;
25use serde_json::{Value, json};
26use std::borrow::Cow;
27use std::sync::{
28 Arc, Mutex,
29 atomic::{AtomicBool, AtomicUsize, Ordering},
30};
31
32struct SubAgentLoopStateRefs {
34 system_prompt: Arc<Mutex<String>>,
35 messages: Arc<Mutex<Vec<ChatMessage>>>,
36 status: Arc<Mutex<SubAgentStatus>>,
37 current_tool: Arc<Mutex<Option<String>>>,
38 tool_calls_count: Arc<AtomicUsize>,
39 current_round: Arc<AtomicUsize>,
40}
41
42impl SubAgentLoopStateRefs {
43 fn from_handle(handle: &SubAgentHandle) -> Self {
44 Self {
45 system_prompt: Arc::clone(&handle.system_prompt),
46 messages: Arc::clone(&handle.messages),
47 status: Arc::clone(&handle.status),
48 current_tool: Arc::clone(&handle.current_tool),
49 tool_calls_count: Arc::clone(&handle.tool_calls_count),
50 current_round: Arc::clone(&handle.current_round),
51 }
52 }
53
54 fn set_status(&self, status: SubAgentStatus) {
55 if let Ok(mut s) = self.status.lock() {
56 *s = status;
57 }
58 }
59
60 fn set_current_tool(&self, name: Option<String>) {
61 if let Ok(mut t) = self.current_tool.lock() {
62 *t = name;
63 }
64 }
65}
66
67struct SubAgentLoopParams {
69 provider: ModelProvider,
70 system_prompt: Option<String>,
71 prompt: String,
72 tools: Vec<ToolDefinition>,
73 registry: Arc<ToolRegistry>,
74 jcli_config: Arc<JcliConfig>,
75 snapshot: Option<SubAgentLoopStateRefs>,
76 description: String,
77 transcript_path: Option<std::path::PathBuf>,
79 context_config: crate::tools::derived_shared::AgentContextConfig,
81 sub_agent_metrics: Arc<Mutex<crate::tools::derived_shared::SubAgentMetrics>>,
83}
84
85fn sanitize_agent_name(description: &str) -> String {
87 let cleaned: String = description
88 .chars()
89 .map(|c| if c.is_whitespace() { '_' } else { c })
90 .collect();
91 if cleaned.chars().count() <= 24 {
93 cleaned
94 } else {
95 let truncated: String = cleaned.chars().take(24).collect();
96 format!("{}…", truncated)
97 }
98}
99
100fn build_sub_agent_system_prompt(base_prompt: Option<&str>) -> String {
105 let template = crate::template::sub_agent_system_prompt_template();
106 let base = base_prompt.unwrap_or("You are a helpful assistant.");
107 template.replace("{{.base_prompt}}", base)
108}
109
110#[derive(Deserialize, JsonSchema)]
112struct AgentParams {
113 prompt: String,
115 #[serde(default)]
117 description: Option<String>,
118 #[serde(default)]
120 run_in_background: bool,
121 #[serde(default)]
125 worktree: bool,
126 #[serde(default)]
129 inherit_permissions: bool,
130}
131
132#[allow(dead_code)]
136pub struct SubAgentTool {
137 pub shared: DerivedAgentShared,
138}
139
140impl SubAgentTool {
141 pub const NAME: &'static str = "Agent";
142}
143
144impl Tool for SubAgentTool {
145 fn name(&self) -> &str {
146 Self::NAME
147 }
148
149 fn description(&self) -> Cow<'_, str> {
150 r#"
151 Launch a sub-agent to handle complex, multi-step tasks autonomously.
152 The sub-agent runs with a fresh context (system prompt + your prompt as user message).
153 It can use all tools except Agent (to prevent recursion).
154
155 When NOT to use the Agent tool:
156 - If you want to read a specific file path, use Read or Glob instead
157 - If you are searching for a specific class/function definition, use Grep or Glob instead
158 - If you are searching code within a specific file or 2-3 files, use Read instead
159
160 Usage notes:
161 - Always include a short description (3-5 words) summarizing what the agent will do
162 - The result returned by the agent is not visible to the user. To show the user the result, send a text message with a concise summary
163 - Use foreground (default) when you need the agent's results before proceeding
164 - Use background when you have genuinely independent work to do in parallel
165 - Clearly tell the agent whether you expect it to write code or just do research (search, file reads, web fetches, etc.)
166 - Provide clear, detailed prompts so the agent can work autonomously — explain what you're trying to accomplish, what you've already learned, and give enough context for the agent to make judgment calls
167 "#.into()
168 }
169
170 fn parameters_schema(&self) -> Value {
171 schema_to_tool_params::<AgentParams>()
172 }
173
174 fn execute(&self, arguments: &str, cancelled: &Arc<AtomicBool>) -> ToolResult {
175 let params: AgentParams = match parse_tool_args(arguments) {
176 Ok(p) => p,
177 Err(e) => return e,
178 };
179
180 let prompt = params.prompt;
181 let description = params
182 .description
183 .unwrap_or_else(|| "sub-agent task".to_string());
184 let run_in_background = params.run_in_background;
185 let use_worktree = params.worktree;
186
187 let provider = safe_lock(&self.shared.provider, "SubAgentTool::provider").clone();
189 let base_prompt =
190 safe_lock(&self.shared.system_prompt, "SubAgentTool::system_prompt").clone();
191 let system_prompt = build_sub_agent_system_prompt(base_prompt.as_deref());
192
193 let worktree_info: Option<(std::path::PathBuf, String)> = if use_worktree {
195 match create_agent_worktree(&description) {
196 Ok(info) => Some(info),
197 Err(e) => {
198 return ToolResult {
199 output: format!("创建 worktree 失败: {}", e),
200 is_error: true,
201 images: vec![],
202 plan_decision: PlanDecision::None,
203 };
204 }
205 }
206 } else {
207 None
208 };
209
210 let sub_id = self.shared.sub_agent_tracker.allocate_id();
212 let session_id_snapshot =
213 safe_lock(&self.shared.session_id, "SubAgentTool::session_id").clone();
214 let session_paths = SessionPaths::new(&session_id_snapshot);
215 let subagent_todos_path = session_paths.subagent_todos_file(&sub_id);
216 let subagent_transcript_path = session_paths.subagent_transcript(&sub_id);
217
218 let (child_registry, _) = self.shared.build_child_registry(subagent_todos_path);
220 let child_registry = Arc::new(child_registry);
221
222 let mut disabled = self.shared.disabled_tools.as_ref().clone();
223 disabled.push(Self::NAME.to_string());
224 let deferred = match self.shared.deferred_tools.lock() {
227 Ok(guard) => guard,
228 Err(e) => e.into_inner(),
229 }
230 .clone();
231 let tools = child_registry.to_llm_tools_non_deferred(&disabled, &deferred);
232
233 let jcli_config = if params.inherit_permissions {
235 let mut cfg = self.shared.jcli_config.as_ref().clone();
236 cfg.permissions.allow_all = true;
237 Arc::new(cfg)
238 } else {
239 Arc::clone(&self.shared.jcli_config)
240 };
241
242 let context_config = safe_lock(
244 &self.shared.agent_context_config,
245 "SubAgentTool::context_config",
246 )
247 .clone();
248
249 if run_in_background {
250 self.shared.sub_agent_tracker.gc_finished();
252 let handle = self.shared.sub_agent_tracker.register_with_id(
253 sub_id.clone(),
254 &description,
255 "background",
256 );
257
258 let (task_id, output_buffer) = self.shared.background_manager.spawn_command(
260 &format!("Agent: {}", description),
261 None,
262 0,
263 Some(Arc::clone(&handle.is_running)), );
265
266 let snap_running = Arc::clone(&handle.is_running);
267 let snapshot_refs = SubAgentLoopStateRefs::from_handle(&handle);
268
269 let bg_manager = Arc::clone(&self.shared.background_manager);
270 let task_id_clone = task_id.clone();
271 let cancelled_clone = Arc::clone(cancelled);
272
273 let description_clone = description.clone();
274 let display_clone = Arc::clone(&self.shared.display_messages);
275 let context_clone = Arc::clone(&self.shared.context_messages);
276 let transcript_path = subagent_transcript_path.clone();
277 let _sub_id_for_thread = sub_id.clone();
278 let context_config_clone = context_config.clone();
279 let agent_identity = format!("SubAgent@{}", sanitize_agent_name(&description));
280 let sub_agent_metrics_clone = Arc::clone(&self.shared.sub_agent_metrics);
281 std::thread::spawn(move || {
282 set_current_agent_name(&agent_identity);
284 set_current_agent_type(AgentType::SubAgent);
285
286 if let Some((ref wt_path, _)) = worktree_info {
288 set_thread_cwd(wt_path);
289 }
290
291 let result = run_sub_agent_loop(
292 SubAgentLoopParams {
293 provider,
294 system_prompt: Some(system_prompt),
295 prompt,
296 tools,
297 registry: child_registry,
298 jcli_config,
299 snapshot: Some(snapshot_refs),
300 description: description_clone.clone(),
301 transcript_path: Some(transcript_path),
302 context_config: context_config_clone,
303 sub_agent_metrics: sub_agent_metrics_clone,
304 },
305 &cancelled_clone,
306 &display_clone,
307 &context_clone,
308 );
309
310 snap_running.store(false, Ordering::Relaxed);
311
312 if let Some((ref wt_path, ref branch)) = worktree_info {
314 remove_agent_worktree(wt_path, branch);
315 }
316
317 {
319 let mut buf = safe_lock(&output_buffer, "SubAgentTool::bg_output");
320 buf.push_str(&result);
321 }
322
323 bg_manager.complete_task(&task_id_clone, "completed", result);
324 });
325
326 ToolResult {
327 output: json!({
328 "task_id": task_id,
329 "sub_id": sub_id,
330 "description": description,
331 "status": "running in background"
332 })
333 .to_string(),
334 is_error: false,
335 images: vec![],
336 plan_decision: PlanDecision::None,
337 }
338 } else {
339 let old_agent_name = current_agent_name();
342 let old_cwd = thread_cwd();
343 let agent_identity = format!("SubAgent@{}", sanitize_agent_name(&description));
344 set_current_agent_name(&agent_identity);
345 set_current_agent_type(AgentType::SubAgent);
346 if let Some((ref wt_path, _)) = worktree_info {
347 set_thread_cwd(wt_path);
348 }
349
350 self.shared.sub_agent_tracker.gc_finished();
352 let handle = self.shared.sub_agent_tracker.register_with_id(
353 sub_id.clone(),
354 &description,
355 "foreground",
356 );
357 let snap_running = Arc::clone(&handle.is_running);
358 let snapshot_refs = SubAgentLoopStateRefs::from_handle(&handle);
359
360 let cancelled_clone = Arc::clone(cancelled);
361 let result = run_sub_agent_loop(
362 SubAgentLoopParams {
363 provider,
364 system_prompt: Some(system_prompt),
365 prompt,
366 tools,
367 registry: child_registry,
368 jcli_config,
369 snapshot: Some(snapshot_refs),
370 description,
371 transcript_path: Some(subagent_transcript_path),
372 context_config,
373 sub_agent_metrics: Arc::clone(&self.shared.sub_agent_metrics),
374 },
375 &cancelled_clone,
376 &self.shared.display_messages,
377 &self.shared.context_messages,
378 );
379
380 snap_running.store(false, Ordering::Relaxed);
381
382 if let Some((ref wt_path, ref branch)) = worktree_info {
384 remove_agent_worktree(wt_path, branch);
385 }
386 set_current_agent_name(&old_agent_name);
387 match old_cwd {
388 Some(p) => set_thread_cwd(&p),
389 None => clear_thread_cwd(),
390 }
391
392 ToolResult {
393 output: result,
394 is_error: false,
395 images: vec![],
396 plan_decision: PlanDecision::None,
397 }
398 }
399 }
400
401 fn requires_confirmation(&self) -> bool {
402 false
403 }
404}
405
406fn run_sub_agent_loop(
414 params: SubAgentLoopParams,
415 cancelled: &Arc<AtomicBool>,
416 display_messages: &Arc<Mutex<Vec<ChatMessage>>>,
417 context_messages: &Arc<Mutex<Vec<ChatMessage>>>,
418) -> String {
419 let agent_name = sanitize_agent_name(¶ms.description);
420 let sender_label = format!("SubAgent@{}", agent_name);
421 let push_display_and_context =
428 |display_content: String, context_content: String, sender: &str| {
429 if let Ok(mut display) = display_messages.lock() {
430 display.push(
431 ChatMessage::text(MessageRole::Assistant, &display_content).with_sender(sender),
432 );
433 }
434 if let Ok(mut context) = context_messages.lock() {
435 context.push(
436 ChatMessage::text(MessageRole::Assistant, &context_content).with_sender(sender),
437 );
438 }
439 };
440 let push_tool_call_to_display = |item: &ToolCallItem, sender: &str| {
442 if let Ok(mut display) = display_messages.lock() {
443 display.push(ChatMessage {
444 role: MessageRole::Assistant,
445 content: String::new(),
446 tool_calls: Some(vec![item.clone()]),
447 tool_call_id: None,
448 images: None,
449 reasoning_content: None,
450 sender_name: Some(sender.to_string()),
451 recipient_name: None,
452 display_hint: DisplayHint::Normal,
453 });
454 }
455 };
456 let push_tool_result_to_display =
458 |result_content: String, tool_call_id: String, sender: &str| {
459 if let Ok(mut display) = display_messages.lock() {
460 display.push(ChatMessage {
461 role: MessageRole::Tool,
462 content: result_content,
463 tool_calls: None,
464 tool_call_id: Some(tool_call_id),
465 images: None,
466 reasoning_content: None,
467 sender_name: Some(sender.to_string()),
468 recipient_name: None,
469 display_hint: DisplayHint::Normal,
470 });
471 }
472 };
473 let max_rounds = 30; if let Some(ref refs) = params.snapshot {
477 refs.set_status(SubAgentStatus::Thinking);
478 }
479
480 let (rt, client) = match create_runtime_and_client(¶ms.provider) {
481 Ok(pair) => pair,
482 Err(e) => {
483 if let Some(ref refs) = params.snapshot {
484 refs.set_status(SubAgentStatus::Error(e.clone()));
485 }
486 return e;
487 }
488 };
489
490 if let Some(ref refs) = params.snapshot
492 && let Ok(mut sp) = refs.system_prompt.lock()
493 {
494 *sp = params.system_prompt.clone().unwrap_or_default();
495 }
496
497 let mut messages: Vec<ChatMessage> = vec![ChatMessage {
498 role: MessageRole::User,
499 content: params.prompt,
500 tool_calls: None,
501 tool_call_id: None,
502 images: None,
503 reasoning_content: None,
504 sender_name: None,
505 recipient_name: None,
506 display_hint: DisplayHint::Normal,
507 }];
508
509 let sync_messages = |msgs: &Vec<ChatMessage>| {
510 if let Some(ref refs) = params.snapshot
511 && let Ok(mut snap) = refs.messages.lock()
512 {
513 *snap = msgs.clone();
514 }
515 };
516
517 let transcript_path = params.transcript_path.clone();
519 let append_to_transcript = |msgs: &[ChatMessage]| {
520 if let Some(ref path) = transcript_path {
521 for m in msgs {
522 let _ = append_event_to_path(path, &SessionEvent::msg(m.clone()));
523 }
524 }
525 };
526
527 sync_messages(&messages);
528 append_to_transcript(&messages);
529
530 let mut final_text = String::new();
531
532 for round in 0..max_rounds {
533 if cancelled.load(Ordering::Relaxed) {
534 if let Some(ref refs) = params.snapshot {
535 refs.set_status(SubAgentStatus::Cancelled);
536 refs.set_current_tool(None);
537 }
538 return format!("{}\n[Sub-agent cancelled]", final_text);
539 }
540
541 if let Some(ref refs) = params.snapshot {
542 refs.current_round.store(round + 1, Ordering::Relaxed);
543 }
544
545 write_info_log("SubAgent", &format!("Round {}/{}", round + 1, max_rounds));
546
547 if let Some(ref refs) = params.snapshot {
549 refs.set_status(SubAgentStatus::Thinking);
550 }
551
552 let status_for_retry = params.snapshot.as_ref().map(|r| Arc::clone(&r.status));
554 let retry_callback = move |attempt: u32, max_attempts: u32, delay_ms: u64, error: &str| {
555 if let Some(ref status_arc) = status_for_retry
556 && let Ok(mut s) = status_arc.lock()
557 {
558 *s = SubAgentStatus::Retrying {
559 attempt,
560 max_attempts,
561 delay_ms,
562 error: error.to_string(),
563 };
564 }
565 };
566
567 let mut api_messages = crate::context::window::select_messages(
570 &messages,
571 params.context_config.max_history_messages,
572 params.context_config.max_context_tokens,
573 params.context_config.compact.keep_recent,
574 ¶ms.context_config.compact.micro_compact_exempt_tools,
575 );
576 if params.context_config.compact.enabled {
577 crate::context::compact::micro_compact(
578 &mut api_messages,
579 params.context_config.compact.keep_recent,
580 ¶ms.context_config.compact.micro_compact_exempt_tools,
581 );
582 }
583
584 let response = match call_llm_non_stream(&LlmNonStreamRequest {
585 rt: &rt,
586 client: &client,
587 provider: ¶ms.provider,
588 messages: &api_messages,
589 tools: ¶ms.tools,
590 system_prompt: params.system_prompt.as_deref(),
591 on_retry: Some(&retry_callback),
592 }) {
593 Ok(r) => {
594 r
596 }
597 Err(e) => {
598 if let Some(ref refs) = params.snapshot {
599 refs.set_status(SubAgentStatus::Error(e.clone()));
600 refs.set_current_tool(None);
601 }
602 return format!("{}\n{}", final_text, e);
603 }
604 };
605
606 let choice = response
608 .choices
609 .into_iter()
610 .next()
611 .expect("call_llm_non_stream validates non-empty choices");
612
613 if let Some(usage) = response.usage
615 && let Ok(mut m) = params.sub_agent_metrics.lock()
616 {
617 m.total_llm_calls += 1;
618 m.total_input_tokens += usage.prompt_tokens;
619 m.total_output_tokens += usage.completion_tokens;
620 }
621
622 let assistant_text = choice.message.content.clone().unwrap_or_default();
623 let reasoning_content = choice.message.reasoning_content.clone();
624 if !assistant_text.is_empty() {
625 write_info_log("SubAgent", &format!("Reply: {}", &assistant_text));
626 push_display_and_context(
630 assistant_text.clone(),
631 format!("<{}>{}</{}>", sender_label, &assistant_text, sender_label),
632 &sender_label,
633 );
634 }
635
636 let is_tool_calls = choice.finish_reason.as_deref() == Some("tool_calls");
638
639 if !is_tool_calls || choice.message.tool_calls.is_none() {
640 final_text = assistant_text.clone();
642 if !assistant_text.is_empty() {
643 let final_msg = ChatMessage::text(MessageRole::Assistant, assistant_text.clone());
644 messages.push(final_msg);
645 if let Some(last) = messages.last() {
646 append_to_transcript(std::slice::from_ref(last));
647 }
648 sync_messages(&messages);
649 }
650 break;
651 }
652
653 let Some(tool_calls) = choice.message.tool_calls.as_ref() else {
655 break;
656 };
657 let tool_items = extract_tool_items(tool_calls);
658 if tool_items.is_empty() {
659 break;
660 }
661
662 for item in &tool_items {
666 if let Ok(mut context) = context_messages.lock() {
668 context.push(
669 ChatMessage::text(
670 MessageRole::Assistant,
671 format!(
672 "<{}>[调用工具 {}]</{}>",
673 sender_label, item.name, sender_label
674 ),
675 )
676 .with_sender(&sender_label),
677 );
678 }
679 push_tool_call_to_display(item, &sender_label);
681 }
682
683 let assistant_msg = ChatMessage {
685 role: MessageRole::Assistant,
686 content: assistant_text,
687 tool_calls: Some(tool_items.clone()),
688 tool_call_id: None,
689 images: None,
690 reasoning_content,
691 sender_name: None,
692 recipient_name: None,
693 display_hint: DisplayHint::Normal,
694 };
695 messages.push(assistant_msg);
696 if let Some(last) = messages.last() {
697 append_to_transcript(std::slice::from_ref(last));
698 }
699
700 for item in &tool_items {
702 if let Some(ref refs) = params.snapshot {
703 refs.set_current_tool(Some(item.name.clone()));
704 refs.set_status(SubAgentStatus::Working);
705 refs.tool_calls_count.fetch_add(1, Ordering::Relaxed);
706 }
707 let result_msg = execute_tool_with_permission(
708 item,
709 &ToolExecContext {
710 registry: ¶ms.registry,
711 jcli_config: ¶ms.jcli_config,
712 cancelled,
713 log_tag: "SubAgent",
714 verbose: true,
715 },
716 );
717
718 if let Ok(mut m) = params.sub_agent_metrics.lock() {
720 m.total_tool_calls += 1;
721 }
722
723 push_tool_result_to_display(
725 result_msg.content.clone(),
726 result_msg.tool_call_id.clone().unwrap_or_default(),
727 &sender_label,
728 );
729 messages.push(result_msg);
730 if let Some(last) = messages.last() {
731 append_to_transcript(std::slice::from_ref(last));
732 }
733 }
734 if let Some(ref refs) = params.snapshot {
735 refs.set_current_tool(None);
736 refs.set_status(SubAgentStatus::Thinking);
738 }
739
740 sync_messages(&messages);
742 }
743
744 push_display_and_context(
747 "[已完成]".to_string(),
748 format!("<{}>[已完成]</{}>", sender_label, sender_label),
749 &sender_label,
750 );
751
752 if let Some(ref refs) = params.snapshot {
753 refs.set_status(SubAgentStatus::Completed);
754 refs.set_current_tool(None);
755 }
756
757 if final_text.is_empty() {
758 "[Sub-agent completed with no text output]".to_string()
759 } else {
760 final_text
761 }
762}