1mod execution;
16mod helpers;
17mod planning;
18mod reflection;
19
20use anyhow::Result;
21use std::collections::HashSet;
22use std::path::Path;
23
24use super::extensions::{self, MemoryVectorContext};
25use super::llm::{self, LlmClient};
26use super::prompt;
27use super::skills::LoadedSkill;
28use super::soul::Soul;
29use super::types::*;
30use skilllite_core::config::EmbeddingConfig;
31
32use execution::{
33 execute_tool_batch_planning, execute_tool_batch_simple,
34 should_suppress_planning_assistant_text, ExecutionState,
35};
36use helpers::build_agent_result;
37use planning::{
38 build_task_focus_message, maybe_save_checkpoint, run_planning_phase, PlanningResult,
39};
40use reflection::{reflect_planning, reflect_simple, ReflectionOutcome};
41
42const MAX_CLARIFICATIONS: usize = 3;
44
45const MAX_CONTEXT_OVERFLOW_RETRIES: usize = 3;
47
48pub async fn run_agent_loop(
53 config: &AgentConfig,
54 initial_messages: Vec<ChatMessage>,
55 user_message: &str,
56 skills: &[LoadedSkill],
57 event_sink: &mut dyn EventSink,
58 session_key: Option<&str>,
59) -> Result<AgentResult> {
60 if config.enable_task_planning {
61 run_with_task_planning(
62 config,
63 initial_messages,
64 user_message,
65 skills,
66 event_sink,
67 session_key,
68 )
69 .await
70 } else {
71 run_simple_loop(
72 config,
73 initial_messages,
74 user_message,
75 skills,
76 event_sink,
77 session_key,
78 )
79 .await
80 }
81}
82
83async fn run_simple_loop(
88 config: &AgentConfig,
89 initial_messages: Vec<ChatMessage>,
90 user_message: &str,
91 skills: &[LoadedSkill],
92 event_sink: &mut dyn EventSink,
93 session_key: Option<&str>,
94) -> Result<AgentResult> {
95 let start_time = std::time::Instant::now();
96 let client = LlmClient::new(&config.api_base, &config.api_key)?;
97 let workspace = Path::new(&config.workspace);
98 let embed_config = EmbeddingConfig::from_env();
99 let embed_ctx = (config.enable_memory_vector && !config.api_key.is_empty()).then_some(
100 MemoryVectorContext {
101 client: &client,
102 embed_config: &embed_config,
103 },
104 );
105
106 let registry = if config.read_only_tools {
107 extensions::ExtensionRegistry::read_only_with_task_planning(
108 config.enable_memory,
109 config.enable_memory_vector,
110 config.enable_task_planning,
111 skills,
112 )
113 } else {
114 extensions::ExtensionRegistry::with_task_planning(
115 config.enable_memory,
116 config.enable_memory_vector,
117 config.enable_task_planning,
118 skills,
119 )
120 };
121 let all_tools = registry.all_tool_definitions();
122
123 let chat_root = skilllite_executor::chat_root();
125 let soul = Soul::auto_load(config.soul_path.as_deref(), &config.workspace);
126 let system_prompt = prompt::build_system_prompt(
127 config.system_prompt.as_deref(),
128 skills,
129 &config.workspace,
130 session_key,
131 config.enable_memory,
132 Some(registry.availability()),
133 Some(&chat_root),
134 soul.as_ref(),
135 config.context_append.as_deref(),
136 );
137 let mut messages = Vec::new();
138 messages.push(ChatMessage::system(&system_prompt));
139 messages.extend(initial_messages);
140 messages.push(ChatMessage::user(user_message));
141
142 let mut documented_skills: HashSet<String> = HashSet::new();
143 let mut state = ExecutionState::new();
144 let mut no_tool_retries = 0usize;
145 let max_no_tool_retries = 3;
146 let mut task_completed = true;
147 let mut clarification_count = 0usize;
148
149 let tools_ref = if all_tools.is_empty() {
150 None
151 } else {
152 Some(all_tools.as_slice())
153 };
154
155 loop {
156 if state.iterations >= config.max_iterations {
157 tracing::warn!(
158 "Agent loop reached max iterations ({})",
159 config.max_iterations
160 );
161 if clarification_count < MAX_CLARIFICATIONS {
162 let req = ClarificationRequest {
163 reason: "max_iterations".into(),
164 message: format!(
165 "已达到最大执行轮次 ({}),任务可能尚未完成。",
166 config.max_iterations
167 ),
168 suggestions: vec!["继续执行更多轮次".into(), "请指定接下来要做什么".into()],
169 };
170 match event_sink.on_clarification_request(&req) {
171 ClarificationResponse::Continue(hint) => {
172 clarification_count += 1;
173 state.iterations = 0;
174 if let Some(h) = hint {
175 messages.push(ChatMessage::user(&h));
176 }
177 continue;
178 }
179 ClarificationResponse::Stop => {}
180 }
181 }
182 task_completed = false;
183 break;
184 }
185 state.iterations += 1;
186
187 let response = match client
189 .chat_completion_stream(
190 &config.model,
191 &messages,
192 tools_ref,
193 config.temperature,
194 event_sink,
195 )
196 .await
197 {
198 Ok(resp) => {
199 state.context_overflow_retries = 0;
200 resp
201 }
202 Err(e) => {
203 if llm::is_context_overflow_error(&e.to_string()) {
204 state.context_overflow_retries += 1;
205 if state.context_overflow_retries >= MAX_CONTEXT_OVERFLOW_RETRIES {
206 tracing::error!(
207 "Context overflow persists after {} retries, giving up",
208 MAX_CONTEXT_OVERFLOW_RETRIES
209 );
210 return Err(e);
211 }
212 let rc = get_tool_result_recovery_max_chars();
213 tracing::warn!(
214 "Context overflow (attempt {}/{}), truncating to {} chars",
215 state.context_overflow_retries,
216 MAX_CONTEXT_OVERFLOW_RETRIES,
217 rc
218 );
219 llm::truncate_tool_messages(&mut messages, rc);
220 continue;
221 }
222 return Err(e);
223 }
224 };
225
226 let choice = response
227 .choices
228 .into_iter()
229 .next()
230 .ok_or_else(|| anyhow::anyhow!("No choices in LLM response"))?;
231 let assistant_content = choice.message.content;
232 let tool_calls = choice.message.tool_calls;
233 let has_tool_calls = tool_calls.as_ref().is_some_and(|tc| !tc.is_empty());
234
235 if let Some(tcs) = tool_calls {
237 messages.push(ChatMessage::assistant_with_tool_calls(
238 assistant_content.as_deref(),
239 tcs,
240 ));
241 } else if let Some(ref content) = assistant_content {
242 messages.push(ChatMessage::assistant(content));
243 }
244
245 if !has_tool_calls {
247 match reflect_simple(
248 &assistant_content,
249 all_tools.len(),
250 state.iterations,
251 &mut no_tool_retries,
252 max_no_tool_retries,
253 event_sink,
254 &mut messages,
255 ) {
256 ReflectionOutcome::Nudge(msg) => {
257 messages.push(ChatMessage::user(&msg));
258 continue;
259 }
260 ReflectionOutcome::Break => {
261 if clarification_count < MAX_CLARIFICATIONS {
262 let req = ClarificationRequest {
263 reason: "no_progress".into(),
264 message: "Agent 多次尝试后无法继续执行任务,可能需要更多信息。".into(),
265 suggestions: vec![
266 "请补充更多细节或换一种方式描述需求".into(),
267 "继续尝试,不做更改".into(),
268 ],
269 };
270 match event_sink.on_clarification_request(&req) {
271 ClarificationResponse::Continue(hint) => {
272 clarification_count += 1;
273 no_tool_retries = 0;
274 if let Some(h) = hint {
275 messages.push(ChatMessage::user(&h));
276 }
277 continue;
278 }
279 ClarificationResponse::Stop => {}
280 }
281 }
282 break;
283 }
284 ReflectionOutcome::Continue => continue,
285 }
286 }
287
288 no_tool_retries = 0;
290 let tool_calls = match messages.last().and_then(|m| m.tool_calls.clone()) {
291 Some(tc) if !tc.is_empty() => tc,
292 _ => continue,
293 };
294
295 let outcome = execute_tool_batch_simple(
296 &tool_calls,
297 ®istry,
298 workspace,
299 event_sink,
300 embed_ctx.as_ref(),
301 &client,
302 &config.model,
303 skills,
304 &mut messages,
305 &mut documented_skills,
306 &mut state,
307 config.max_consecutive_failures,
308 session_key,
309 )
310 .await;
311
312 if outcome.disclosure_injected {
313 continue;
314 }
315 if outcome.failure_limit_reached {
316 tracing::warn!(
317 "Stopping: {} consecutive tool failures",
318 state.consecutive_failures
319 );
320 if clarification_count < MAX_CLARIFICATIONS {
321 let req = ClarificationRequest {
322 reason: "too_many_failures".into(),
323 message: format!(
324 "工具执行连续失败 {} 次,可能遇到了环境或权限问题。",
325 state.consecutive_failures
326 ),
327 suggestions: vec![
328 "跳过失败的步骤,继续后续任务".into(),
329 "请补充信息帮助 Agent 解决问题".into(),
330 ],
331 };
332 match event_sink.on_clarification_request(&req) {
333 ClarificationResponse::Continue(hint) => {
334 clarification_count += 1;
335 state.consecutive_failures = 0;
336 if let Some(h) = hint {
337 messages.push(ChatMessage::user(&h));
338 }
339 continue;
340 }
341 ClarificationResponse::Stop => {}
342 }
343 }
344 task_completed = false;
345 break;
346 }
347
348 if state.total_tool_calls >= config.max_iterations * config.max_tool_calls_per_task {
350 tracing::warn!("Agent loop reached total tool call limit");
351 if clarification_count < MAX_CLARIFICATIONS {
352 let req = ClarificationRequest {
353 reason: "tool_call_limit".into(),
354 message: "已达到工具调用次数上限,任务可能尚未完成。".into(),
355 suggestions: vec!["继续执行".into(), "请指定接下来要做什么".into()],
356 };
357 match event_sink.on_clarification_request(&req) {
358 ClarificationResponse::Continue(hint) => {
359 clarification_count += 1;
360 if let Some(h) = hint {
361 messages.push(ChatMessage::user(&h));
362 }
363 continue;
364 }
365 ClarificationResponse::Stop => {}
366 }
367 }
368 task_completed = false;
369 break;
370 }
371 }
372
373 let feedback = ExecutionFeedback {
374 total_tools: state.total_tool_calls,
375 failed_tools: state.failed_tool_calls,
376 replans: 0,
377 iterations: state.iterations,
378 elapsed_ms: start_time.elapsed().as_millis() as u64,
379 context_overflow_retries: state.context_overflow_retries,
380 task_completed,
381 task_description: Some(user_message.to_string()),
382 rules_used: state.rules_used,
383 tools_detail: state.tools_detail,
384 };
385 Ok(build_agent_result(
386 messages,
387 state.total_tool_calls,
388 state.iterations,
389 Vec::new(),
390 feedback,
391 ))
392}
393
394async fn run_with_task_planning(
401 config: &AgentConfig,
402 initial_messages: Vec<ChatMessage>,
403 user_message: &str,
404 skills: &[LoadedSkill],
405 event_sink: &mut dyn EventSink,
406 session_key: Option<&str>,
407) -> Result<AgentResult> {
408 let start_time = std::time::Instant::now();
409 let client = LlmClient::new(&config.api_base, &config.api_key)?;
410 let workspace = Path::new(&config.workspace);
411 let embed_config = EmbeddingConfig::from_env();
412 let embed_ctx = (config.enable_memory_vector && !config.api_key.is_empty()).then_some(
413 MemoryVectorContext {
414 client: &client,
415 embed_config: &embed_config,
416 },
417 );
418
419 let registry = if config.read_only_tools {
420 extensions::ExtensionRegistry::read_only_with_task_planning(
421 config.enable_memory,
422 config.enable_memory_vector,
423 config.enable_task_planning,
424 skills,
425 )
426 } else {
427 extensions::ExtensionRegistry::with_task_planning(
428 config.enable_memory,
429 config.enable_memory_vector,
430 config.enable_task_planning,
431 skills,
432 )
433 };
434 let all_tools = registry.all_tool_definitions();
435
436 let PlanningResult {
438 mut planner,
439 mut messages,
440 chat_root,
441 ..
442 } = run_planning_phase(
443 config,
444 initial_messages,
445 user_message,
446 skills,
447 registry.availability(),
448 event_sink,
449 session_key,
450 &client,
451 workspace,
452 )
453 .await?;
454
455 let mut state = ExecutionState::new();
456 let mut documented_skills: HashSet<String> = HashSet::new();
457 let mut consecutive_no_tool = 0usize;
458 let max_no_tool_retries = 3;
459 let mut clarification_count = 0usize;
460
461 let num_tasks = planner.task_list.len();
466 let effective_max = if num_tasks == 0 {
467 config.max_iterations
468 } else {
469 config
470 .max_iterations
471 .min((num_tasks * config.max_tool_calls_per_task).max(config.max_tool_calls_per_task))
472 };
473
474 let tools_ref = if all_tools.is_empty() {
475 None
476 } else {
477 Some(all_tools.as_slice())
478 };
479
480 loop {
481 if state.iterations >= effective_max {
482 tracing::warn!(
483 "Agent loop reached effective max iterations ({})",
484 effective_max
485 );
486 if clarification_count < MAX_CLARIFICATIONS {
487 let req = ClarificationRequest {
488 reason: "max_iterations".into(),
489 message: format!("已达到最大执行轮次 ({}),任务可能尚未完成。", effective_max),
490 suggestions: vec!["继续执行更多轮次".into(), "请指定接下来要做什么".into()],
491 };
492 match event_sink.on_clarification_request(&req) {
493 ClarificationResponse::Continue(hint) => {
494 clarification_count += 1;
495 state.iterations = 0;
496 if let Some(h) = hint {
497 messages.push(ChatMessage::user(&h));
498 }
499 continue;
500 }
501 ClarificationResponse::Stop => {}
502 }
503 }
504 break;
505 }
506 state.iterations += 1;
507
508 let suppress_stream = !planner.all_completed() && planner.current_task().is_some();
513
514 let llm_result = if suppress_stream {
516 client
517 .chat_completion(&config.model, &messages, tools_ref, config.temperature)
518 .await
519 } else {
520 client
521 .chat_completion_stream(
522 &config.model,
523 &messages,
524 tools_ref,
525 config.temperature,
526 event_sink,
527 )
528 .await
529 };
530
531 let response = match llm_result {
532 Ok(resp) => {
533 state.context_overflow_retries = 0;
534 resp
535 }
536 Err(e) => {
537 if llm::is_context_overflow_error(&e.to_string()) {
538 state.context_overflow_retries += 1;
539 if state.context_overflow_retries >= MAX_CONTEXT_OVERFLOW_RETRIES {
540 tracing::error!(
541 "Context overflow persists after {} retries, giving up",
542 MAX_CONTEXT_OVERFLOW_RETRIES
543 );
544 return Err(e);
545 }
546 let rc = get_tool_result_recovery_max_chars();
547 tracing::warn!(
548 "Context overflow (attempt {}/{}), truncating to {} chars",
549 state.context_overflow_retries,
550 MAX_CONTEXT_OVERFLOW_RETRIES,
551 rc
552 );
553 llm::truncate_tool_messages(&mut messages, rc);
554 continue;
555 }
556 return Err(e);
557 }
558 };
559
560 let choice = response
561 .choices
562 .into_iter()
563 .next()
564 .ok_or_else(|| anyhow::anyhow!("No choices in LLM response"))?;
565 let mut assistant_content = choice.message.content;
566 let tool_calls = choice.message.tool_calls;
567 let has_tool_calls = tool_calls.as_ref().is_some_and(|tc| !tc.is_empty());
568 let suppressed_planning_text =
569 should_suppress_planning_assistant_text(&planner, has_tool_calls)
570 && assistant_content
571 .as_ref()
572 .is_some_and(|content| !content.trim().is_empty());
573 if suppressed_planning_text {
574 tracing::info!("Suppressed free-form assistant text during pending task execution");
575 assistant_content = None;
576 }
577
578 if let Some(tcs) = tool_calls {
579 messages.push(ChatMessage::assistant_with_tool_calls(
580 assistant_content.as_deref(),
581 tcs,
582 ));
583 } else if let Some(ref content) = assistant_content {
584 messages.push(ChatMessage::assistant(content));
585 }
586
587 if suppress_stream && has_tool_calls {
589 if let Some(ref content) = assistant_content {
590 event_sink.on_text(content);
591 }
592 }
593
594 if !has_tool_calls {
596 match reflect_planning(
597 &assistant_content,
598 suppress_stream,
599 &mut planner,
600 &mut consecutive_no_tool,
601 max_no_tool_retries,
602 event_sink,
603 &mut messages,
604 ) {
605 ReflectionOutcome::Nudge(msg) => {
606 messages.push(ChatMessage::user(&msg));
607 continue;
608 }
609 ReflectionOutcome::Break => {
610 if !planner.all_completed() && clarification_count < MAX_CLARIFICATIONS {
611 let req = ClarificationRequest {
612 reason: "no_progress".into(),
613 message: "Agent 多次尝试后无法继续执行任务,可能需要更多信息。".into(),
614 suggestions: vec![
615 "请补充更多细节或换一种方式描述需求".into(),
616 "继续尝试,不做更改".into(),
617 ],
618 };
619 match event_sink.on_clarification_request(&req) {
620 ClarificationResponse::Continue(hint) => {
621 clarification_count += 1;
622 consecutive_no_tool = 0;
623 if let Some(h) = hint {
624 messages.push(ChatMessage::user(&h));
625 }
626 continue;
627 }
628 ClarificationResponse::Stop => {}
629 }
630 }
631 break;
632 }
633 _ => continue,
634 }
635 }
636
637 consecutive_no_tool = 0;
639 let tool_calls = match messages.last().and_then(|m| m.tool_calls.clone()) {
640 Some(tc) if !tc.is_empty() => tc,
641 _ => continue,
642 };
643
644 let outcome = execute_tool_batch_planning(
645 &tool_calls,
646 ®istry,
647 workspace,
648 event_sink,
649 embed_ctx.as_ref(),
650 &client,
651 &config.model,
652 &mut planner,
653 skills,
654 &mut messages,
655 &mut documented_skills,
656 &mut state,
657 config.max_tool_calls_per_task,
658 config.max_consecutive_failures,
659 session_key,
660 )
661 .await;
662
663 if outcome.disclosure_injected {
664 continue;
665 }
666 if outcome.failure_limit_reached {
667 tracing::warn!(
668 "Stopping: {} consecutive tool failures",
669 state.consecutive_failures
670 );
671 if clarification_count < MAX_CLARIFICATIONS {
672 let req = ClarificationRequest {
673 reason: "too_many_failures".into(),
674 message: format!(
675 "工具执行连续失败 {} 次,可能遇到了环境或权限问题。",
676 state.consecutive_failures
677 ),
678 suggestions: vec![
679 "跳过失败的步骤,继续后续任务".into(),
680 "请补充信息帮助 Agent 解决问题".into(),
681 ],
682 };
683 match event_sink.on_clarification_request(&req) {
684 ClarificationResponse::Continue(hint) => {
685 clarification_count += 1;
686 state.consecutive_failures = 0;
687 if let Some(h) = hint {
688 messages.push(ChatMessage::user(&h));
689 }
690 continue;
691 }
692 ClarificationResponse::Stop => {}
693 }
694 }
695 break;
696 }
697 if suppressed_planning_text && !planner.all_completed() {
698 if let Some(nudge) = planner.build_nudge_message() {
699 messages.push(ChatMessage::user(&format!(
700 "Pending tasks still exist. During execution, do not use free-form completion or wrap-up text. \
701 Complete the current task structurally with `complete_task`, then continue.\n\n{}",
702 nudge
703 )));
704 }
705 }
706 if outcome.depth_limit_reached {
707 let depth_msg = planner.build_depth_limit_message(config.max_tool_calls_per_task);
708 messages.push(ChatMessage::user(&depth_msg));
709 state.tool_calls_current_task = 0; }
711
712 if planner.all_completed() {
717 tracing::info!("All tasks completed, ending iteration");
718 let has_substantial = assistant_content
719 .as_ref()
720 .is_some_and(|c| c.trim().len() > 50);
721 if !has_substantial {
722 if let Ok(resp) = client
723 .chat_completion_stream(
724 &config.model,
725 &messages,
726 None,
727 config.temperature,
728 event_sink,
729 )
730 .await
731 {
732 if let Some(ch) = resp.choices.into_iter().next() {
733 if let Some(ref content) = ch.message.content {
734 event_sink.on_text(content);
735 messages.push(ChatMessage::assistant(content));
736 }
737 }
738 }
739 }
740 break;
741 }
742
743 maybe_save_checkpoint(
745 session_key,
746 user_message,
747 config,
748 &planner,
749 &messages,
750 &chat_root,
751 );
752
753 let tools_called: Vec<String> = {
755 let mut seen = HashSet::new();
756 let mut result = Vec::new();
757 for d in state.tools_detail.iter().filter(|d| d.success) {
758 if seen.insert(d.tool.as_str()) {
759 result.push(d.tool.clone());
760 }
761 }
762 result
763 };
764 if let Some(focus_msg) = build_task_focus_message(&planner, &tools_called) {
765 messages.push(ChatMessage::system(&focus_msg));
766 }
767
768 if state.total_tool_calls >= effective_max * config.max_tool_calls_per_task {
770 tracing::warn!("Agent loop reached total tool call limit");
771 if clarification_count < MAX_CLARIFICATIONS {
772 let req = ClarificationRequest {
773 reason: "tool_call_limit".into(),
774 message: "已达到工具调用次数上限,任务可能尚未完成。".into(),
775 suggestions: vec!["继续执行".into(), "请指定接下来要做什么".into()],
776 };
777 match event_sink.on_clarification_request(&req) {
778 ClarificationResponse::Continue(hint) => {
779 clarification_count += 1;
780 if let Some(h) = hint {
781 messages.push(ChatMessage::user(&h));
782 }
783 continue;
784 }
785 ClarificationResponse::Stop => {}
786 }
787 }
788 break;
789 }
790 }
791
792 let feedback = ExecutionFeedback {
793 total_tools: state.total_tool_calls,
794 failed_tools: state.failed_tool_calls,
795 replans: state.replan_count,
796 iterations: state.iterations,
797 elapsed_ms: start_time.elapsed().as_millis() as u64,
798 context_overflow_retries: state.context_overflow_retries,
799 task_completed: planner.all_completed(),
800 task_description: Some(user_message.to_string()),
801 rules_used: planner.matched_rule_ids().to_vec(),
802 tools_detail: state.tools_detail,
803 };
804
805 Ok(build_agent_result(
806 messages,
807 state.total_tool_calls,
808 state.iterations,
809 planner.task_list,
810 feedback,
811 ))
812}