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