jarvis-ai-assistant 0.3.26__py3-none-any.whl → 0.3.28__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +303 -177
  3. jarvis/jarvis_agent/agent_manager.py +6 -0
  4. jarvis/jarvis_agent/config.py +92 -0
  5. jarvis/jarvis_agent/config_editor.py +1 -1
  6. jarvis/jarvis_agent/event_bus.py +48 -0
  7. jarvis/jarvis_agent/file_methodology_manager.py +1 -3
  8. jarvis/jarvis_agent/jarvis.py +77 -36
  9. jarvis/jarvis_agent/memory_manager.py +70 -3
  10. jarvis/jarvis_agent/prompt_manager.py +82 -0
  11. jarvis/jarvis_agent/run_loop.py +130 -0
  12. jarvis/jarvis_agent/shell_input_handler.py +1 -1
  13. jarvis/jarvis_agent/task_analyzer.py +89 -11
  14. jarvis/jarvis_agent/task_manager.py +26 -0
  15. jarvis/jarvis_agent/user_interaction.py +42 -0
  16. jarvis/jarvis_code_agent/code_agent.py +18 -3
  17. jarvis/jarvis_code_agent/lint.py +5 -5
  18. jarvis/jarvis_code_analysis/code_review.py +0 -1
  19. jarvis/jarvis_data/config_schema.json +7 -6
  20. jarvis/jarvis_git_squash/main.py +6 -1
  21. jarvis/jarvis_git_utils/git_commiter.py +51 -16
  22. jarvis/jarvis_mcp/stdio_mcp_client.py +1 -1
  23. jarvis/jarvis_memory_organizer/memory_organizer.py +2 -5
  24. jarvis/jarvis_methodology/main.py +0 -2
  25. jarvis/jarvis_multi_agent/__init__.py +3 -3
  26. jarvis/jarvis_platform/base.py +5 -6
  27. jarvis/jarvis_platform/registry.py +1 -1
  28. jarvis/jarvis_platform/yuanbao.py +0 -1
  29. jarvis/jarvis_platform_manager/main.py +28 -11
  30. jarvis/jarvis_platform_manager/service.py +1 -1
  31. jarvis/jarvis_rag/cli.py +1 -1
  32. jarvis/jarvis_rag/embedding_manager.py +0 -1
  33. jarvis/jarvis_rag/llm_interface.py +0 -3
  34. jarvis/jarvis_smart_shell/main.py +0 -1
  35. jarvis/jarvis_stats/cli.py +15 -35
  36. jarvis/jarvis_stats/stats.py +178 -51
  37. jarvis/jarvis_tools/clear_memory.py +1 -3
  38. jarvis/jarvis_tools/cli/main.py +0 -1
  39. jarvis/jarvis_tools/edit_file.py +0 -1
  40. jarvis/jarvis_tools/generate_new_tool.py +3 -5
  41. jarvis/jarvis_tools/registry.py +17 -3
  42. jarvis/jarvis_tools/retrieve_memory.py +2 -3
  43. jarvis/jarvis_tools/save_memory.py +3 -3
  44. jarvis/jarvis_tools/search_web.py +2 -2
  45. jarvis/jarvis_tools/sub_agent.py +114 -85
  46. jarvis/jarvis_tools/sub_code_agent.py +29 -7
  47. jarvis/jarvis_tools/virtual_tty.py +3 -14
  48. jarvis/jarvis_utils/builtin_replace_map.py +4 -4
  49. jarvis/jarvis_utils/config.py +44 -15
  50. jarvis/jarvis_utils/fzf.py +56 -0
  51. jarvis/jarvis_utils/git_utils.py +1 -1
  52. jarvis/jarvis_utils/globals.py +1 -2
  53. jarvis/jarvis_utils/input.py +0 -3
  54. jarvis/jarvis_utils/methodology.py +3 -5
  55. jarvis/jarvis_utils/output.py +1 -1
  56. jarvis/jarvis_utils/utils.py +117 -27
  57. {jarvis_ai_assistant-0.3.26.dist-info → jarvis_ai_assistant-0.3.28.dist-info}/METADATA +2 -3
  58. {jarvis_ai_assistant-0.3.26.dist-info → jarvis_ai_assistant-0.3.28.dist-info}/RECORD +62 -56
  59. {jarvis_ai_assistant-0.3.26.dist-info → jarvis_ai_assistant-0.3.28.dist-info}/WHEEL +0 -0
  60. {jarvis_ai_assistant-0.3.26.dist-info → jarvis_ai_assistant-0.3.28.dist-info}/entry_points.txt +0 -0
  61. {jarvis_ai_assistant-0.3.26.dist-info → jarvis_ai_assistant-0.3.28.dist-info}/licenses/LICENSE +0 -0
  62. {jarvis_ai_assistant-0.3.26.dist-info → jarvis_ai_assistant-0.3.28.dist-info}/top_level.txt +0 -0
@@ -3,10 +3,16 @@
3
3
  import datetime
4
4
  import os
5
5
  import platform
6
+ import re
6
7
  from pathlib import Path
8
+ from enum import Enum
7
9
  from typing import Any, Callable, Dict, List, Optional, Protocol, Tuple, Union
8
10
 
9
11
  # 第三方库导入
12
+ from rich.align import Align
13
+ from rich.console import Console
14
+ from rich.panel import Panel
15
+ from rich.text import Text
10
16
 
11
17
  # 本地库导入
12
18
  # jarvis_agent 相关
@@ -23,6 +29,13 @@ from jarvis.jarvis_agent.prompts import (
23
29
  SUMMARY_REQUEST_PROMPT,
24
30
  TASK_ANALYSIS_PROMPT,
25
31
  )
32
+ from jarvis.jarvis_tools.registry import ToolRegistry
33
+ from jarvis.jarvis_agent.prompt_manager import PromptManager
34
+ from jarvis.jarvis_agent.event_bus import EventBus
35
+ from jarvis.jarvis_agent.config import AgentConfig
36
+ from jarvis.jarvis_agent.run_loop import AgentRunLoop
37
+ from jarvis.jarvis_agent.user_interaction import UserInteractionHandler
38
+ from jarvis.jarvis_utils.methodology import _load_all_methodologies
26
39
 
27
40
  # jarvis_platform 相关
28
41
  from jarvis.jarvis_platform.base import BasePlatform
@@ -54,7 +67,10 @@ from jarvis.jarvis_utils.tag import ot
54
67
 
55
68
 
56
69
  def show_agent_startup_stats(
57
- agent_name: str, model_name: str, tool_registry_instance: Optional[Any] = None
70
+ agent_name: str,
71
+ model_name: str,
72
+ tool_registry_instance: Optional[Any] = None,
73
+ platform_name: Optional[str] = None,
58
74
  ) -> None:
59
75
  """输出启动时的统计信息
60
76
 
@@ -63,16 +79,6 @@ def show_agent_startup_stats(
63
79
  model_name: 使用的模型名称
64
80
  """
65
81
  try:
66
- from jarvis.jarvis_utils.methodology import _load_all_methodologies
67
- from jarvis.jarvis_tools.registry import ToolRegistry
68
- from jarvis.jarvis_utils.config import get_data_dir
69
- from pathlib import Path
70
- from rich.console import Console
71
- from rich.panel import Panel
72
- from rich.text import Text
73
- from rich.align import Align
74
- import os
75
-
76
82
  methodologies = _load_all_methodologies()
77
83
  methodology_count = len(methodologies)
78
84
 
@@ -109,7 +115,8 @@ def show_agent_startup_stats(
109
115
  current_dir = os.getcwd()
110
116
 
111
117
  # 构建欢迎信息
112
- welcome_message = f"{agent_name} 初始化完成 - 使用 {model_name} 模型"
118
+ platform = platform_name or get_normal_platform_name()
119
+ welcome_message = f"{agent_name} 初始化完成 - 使用 {platform} 平台 {model_name} 模型"
113
120
 
114
121
  stats_parts = [
115
122
  f"📚 本地方法论: [bold cyan]{methodology_count}[/bold cyan]",
@@ -198,12 +205,23 @@ origin_agent_system_prompt = f"""
198
205
  """
199
206
 
200
207
 
208
+ class LoopAction(Enum):
209
+ SKIP_TURN = "skip_turn"
210
+ CONTINUE = "continue"
211
+ COMPLETE = "complete"
212
+
213
+
201
214
  class Agent:
202
215
  def clear_history(self):
203
216
  """
204
217
  Clears the current conversation history by delegating to the session manager.
205
218
  """
206
219
  self.session.clear_history()
220
+ # 广播清理历史后的事件
221
+ try:
222
+ self.event_bus.emit("after_history_clear", agent=self)
223
+ except Exception:
224
+ pass
207
225
 
208
226
  def __del__(self):
209
227
  # 只有在记录启动时才停止记录
@@ -279,15 +297,16 @@ class Agent:
279
297
  self._init_session()
280
298
 
281
299
  # 初始化处理器
282
- safe_output_handlers: List[OutputHandlerProtocol] = []
283
- if output_handler:
284
- safe_output_handlers = output_handler
285
- safe_use_tools: List[str] = []
286
- if use_tools:
287
- safe_use_tools = use_tools
288
300
  self._init_handlers(
289
- safe_output_handlers, input_handler, multiline_inputer, safe_use_tools
301
+ output_handler or [],
302
+ input_handler,
303
+ multiline_inputer,
304
+ use_tools or [],
290
305
  )
306
+ # 初始化用户交互封装,保持向后兼容
307
+ self.user_interaction = UserInteractionHandler(self.multiline_inputer, self.user_confirm)
308
+ # 将确认函数指向封装后的 confirm,保持既有调用不变
309
+ self.user_confirm = self.user_interaction.confirm # type: ignore[assignment]
291
310
 
292
311
  # 初始化配置
293
312
  self._init_config(
@@ -299,16 +318,24 @@ class Agent:
299
318
  force_save_memory,
300
319
  )
301
320
 
321
+ # 初始化事件总线需先于管理器,以便管理器在构造中安全订阅事件
322
+ self.event_bus = EventBus()
302
323
  # 初始化管理器
303
324
  self.memory_manager = MemoryManager(self)
304
325
  self.task_analyzer = TaskAnalyzer(self)
305
326
  self.file_methodology_manager = FileMethodologyManager(self)
327
+ self.prompt_manager = PromptManager(self)
306
328
 
307
329
  # 设置系统提示词
308
330
  self._setup_system_prompt()
309
331
 
310
332
  # 输出统计信息(包含欢迎信息)
311
- show_agent_startup_stats(name, self.model.name(), self.get_tool_registry()) # type: ignore
333
+ show_agent_startup_stats(
334
+ name,
335
+ self.model.name(),
336
+ self.get_tool_registry(), # type: ignore
337
+ platform_name=self.model.platform_name(), # type: ignore
338
+ )
312
339
 
313
340
  def _init_model(self, model_group: Optional[str]):
314
341
  """初始化模型平台(统一使用 normal 平台/模型)"""
@@ -340,16 +367,10 @@ class Agent:
340
367
  use_tools: List[str],
341
368
  ):
342
369
  """初始化各种处理器"""
343
- from jarvis.jarvis_tools.registry import ToolRegistry
344
-
345
- self.output_handler = output_handler if output_handler else [ToolRegistry()]
370
+ self.output_handler = output_handler or [ToolRegistry()]
346
371
  self.set_use_tools(use_tools)
347
-
348
- self.input_handler = input_handler if input_handler is not None else []
349
-
350
- self.multiline_inputer = (
351
- multiline_inputer if multiline_inputer else get_multiline_input
352
- )
372
+ self.input_handler = input_handler or []
373
+ self.multiline_inputer = multiline_inputer or get_multiline_input
353
374
 
354
375
  def _init_config(
355
376
  self,
@@ -361,47 +382,57 @@ class Agent:
361
382
  force_save_memory: Optional[bool],
362
383
  ):
363
384
  """初始化配置选项"""
364
- # 如果有上传文件,自动禁用方法论
365
- self.use_methodology = (
366
- False
367
- if self.files
368
- else (
369
- use_methodology if use_methodology is not None else is_use_methodology()
370
- )
371
- )
372
-
373
- self.use_analysis = (
374
- use_analysis if use_analysis is not None else is_use_analysis()
375
- )
376
-
377
- self.execute_tool_confirm = (
378
- execute_tool_confirm
379
- if execute_tool_confirm is not None
380
- else is_execute_tool_confirm()
381
- )
382
-
383
- self.summary_prompt = (
384
- summary_prompt if summary_prompt else DEFAULT_SUMMARY_PROMPT
385
- )
386
-
387
- self.max_token_count = get_max_token_count(model_group)
388
-
389
- self.force_save_memory = (
390
- force_save_memory
391
- if force_save_memory is not None
392
- else is_force_save_memory()
393
- )
385
+ # 使用集中配置解析,保持与原逻辑一致
386
+ cfg = AgentConfig(
387
+ system_prompt=self.system_prompt,
388
+ name=self.name,
389
+ description=self.description,
390
+ model_group=model_group,
391
+ auto_complete=self.auto_complete,
392
+ need_summary=self.need_summary,
393
+ summary_prompt=summary_prompt,
394
+ execute_tool_confirm=execute_tool_confirm,
395
+ use_methodology=use_methodology,
396
+ use_analysis=use_analysis,
397
+ force_save_memory=force_save_memory,
398
+ files=self.files,
399
+ max_token_count=None,
400
+ ).resolve_defaults()
401
+
402
+ # 将解析结果回填到 Agent 实例属性,保持向后兼容
403
+ self.use_methodology = bool(cfg.use_methodology)
404
+ self.use_analysis = bool(cfg.use_analysis)
405
+ self.execute_tool_confirm = bool(cfg.execute_tool_confirm)
406
+ self.summary_prompt = cfg.summary_prompt or DEFAULT_SUMMARY_PROMPT
407
+ self.max_token_count = int(cfg.max_token_count or get_max_token_count(model_group))
408
+ self.force_save_memory = bool(cfg.force_save_memory)
409
+
410
+ # 聚合配置到 AgentConfig,作为后续单一事实来源(保持兼容,不改变既有属性使用)
411
+ self.config = cfg
394
412
 
395
413
  def _setup_system_prompt(self):
396
414
  """设置系统提示词"""
397
- action_prompt = self.get_tool_usage_prompt()
398
- self.model.set_system_prompt( # type: ignore
399
- f"""
415
+ try:
416
+ if hasattr(self, "prompt_manager"):
417
+ prompt_text = self.prompt_manager.build_system_prompt()
418
+ else:
419
+ action_prompt = self.get_tool_usage_prompt()
420
+ prompt_text = f"""
400
421
  {self.system_prompt}
401
422
 
402
423
  {action_prompt}
403
424
  """
404
- )
425
+ self.model.set_system_prompt(prompt_text) # type: ignore
426
+ except Exception:
427
+ # 回退到原始行为,确保兼容性
428
+ action_prompt = self.get_tool_usage_prompt()
429
+ self.model.set_system_prompt( # type: ignore
430
+ f"""
431
+ {self.system_prompt}
432
+
433
+ {action_prompt}
434
+ """
435
+ )
405
436
 
406
437
  def set_user_data(self, key: str, value: Any):
407
438
  """Sets user data in the session."""
@@ -413,8 +444,6 @@ class Agent:
413
444
 
414
445
  def set_use_tools(self, use_tools):
415
446
  """设置要使用的工具列表"""
416
- from jarvis.jarvis_tools.registry import ToolRegistry
417
-
418
447
  for handler in self.output_handler:
419
448
  if isinstance(handler, ToolRegistry):
420
449
  if use_tools:
@@ -435,6 +464,9 @@ class Agent:
435
464
  If the configured multiline_inputer supports 'print_on_empty' keyword, pass it;
436
465
  otherwise, fall back to calling with a single argument for compatibility.
437
466
  """
467
+ # 优先通过用户交互封装,便于未来替换 UI
468
+ if hasattr(self, "user_interaction"):
469
+ return self.user_interaction.multiline_input(tip, print_on_empty)
438
470
  try:
439
471
  # Try to pass the keyword for enhanced input handler
440
472
  return self.multiline_inputer(tip, print_on_empty=print_on_empty) # type: ignore
@@ -463,13 +495,15 @@ class Agent:
463
495
 
464
496
  def get_tool_registry(self) -> Optional[Any]:
465
497
  """获取工具注册表实例"""
466
- from jarvis.jarvis_tools.registry import ToolRegistry
467
-
468
498
  for handler in self.output_handler:
469
499
  if isinstance(handler, ToolRegistry):
470
500
  return handler
471
501
  return None
472
502
 
503
+ def get_event_bus(self) -> EventBus:
504
+ """获取事件总线实例"""
505
+ return self.event_bus
506
+
473
507
  def _call_model(
474
508
  self, message: str, need_complete: bool = False, run_input_handlers: bool = True
475
509
  ) -> str:
@@ -518,11 +552,38 @@ class Agent:
518
552
 
519
553
  def _add_addon_prompt(self, message: str, need_complete: bool) -> str:
520
554
  """添加附加提示到消息"""
555
+ # 广播添加附加提示前事件(不影响主流程)
556
+ try:
557
+ self.event_bus.emit(
558
+ "before_addon_prompt",
559
+ agent=self,
560
+ need_complete=need_complete,
561
+ current_message=message,
562
+ has_session_addon=bool(self.session.addon_prompt),
563
+ )
564
+ except Exception:
565
+ pass
566
+
567
+ addon_text = ""
521
568
  if self.session.addon_prompt:
522
- message += f"\n\n{self.session.addon_prompt}"
569
+ addon_text = self.session.addon_prompt
570
+ message += f"\n\n{addon_text}"
523
571
  self.session.addon_prompt = ""
524
572
  else:
525
- message += f"\n\n{self.make_default_addon_prompt(need_complete)}"
573
+ addon_text = self.make_default_addon_prompt(need_complete)
574
+ message += f"\n\n{addon_text}"
575
+
576
+ # 广播添加附加提示后事件(不影响主流程)
577
+ try:
578
+ self.event_bus.emit(
579
+ "after_addon_prompt",
580
+ agent=self,
581
+ need_complete=need_complete,
582
+ addon_text=addon_text,
583
+ final_message=message,
584
+ )
585
+ except Exception:
586
+ pass
526
587
  return message
527
588
 
528
589
  def _manage_conversation_length(self, message: str) -> str:
@@ -542,7 +603,29 @@ class Agent:
542
603
  if not self.model:
543
604
  raise RuntimeError("Model not initialized")
544
605
 
606
+ # 事件:模型调用前
607
+ try:
608
+ self.event_bus.emit(
609
+ "before_model_call",
610
+ agent=self,
611
+ message=message,
612
+ )
613
+ except Exception:
614
+ pass
615
+
545
616
  response = self.model.chat_until_success(message) # type: ignore
617
+
618
+ # 事件:模型调用后
619
+ try:
620
+ self.event_bus.emit(
621
+ "after_model_call",
622
+ agent=self,
623
+ message=message,
624
+ response=response,
625
+ )
626
+ except Exception:
627
+ pass
628
+
546
629
  self.session.conversation_length += get_context_token_count(response)
547
630
 
548
631
  return response
@@ -565,7 +648,7 @@ class Agent:
565
648
  ) # type: ignore
566
649
 
567
650
  return summary
568
- except Exception as e:
651
+ except Exception:
569
652
  PrettyOutput.print("总结对话历史失败", OutputType.ERROR)
570
653
  return ""
571
654
 
@@ -586,12 +669,11 @@ class Agent:
586
669
  注意:
587
670
  当上下文长度超过最大值时使用
588
671
  """
589
- # 在清理历史之前,提示用户保存重要记忆
672
+ # 在清理历史之前,提示用户保存重要记忆(事件驱动触发实际保存)
590
673
  if self.force_save_memory:
591
674
  PrettyOutput.print(
592
675
  "对话历史即将被总结和清理,请先保存重要信息...", OutputType.INFO
593
676
  )
594
- self.memory_manager.prompt_memory_save()
595
677
 
596
678
  if self._should_use_file_upload():
597
679
  return self._handle_history_with_file_upload()
@@ -613,17 +695,38 @@ class Agent:
613
695
 
614
696
  # 清理历史(但不清理prompt,因为prompt会在builtin_input_handler中设置)
615
697
  if self.model:
698
+ # 广播清理历史前事件
699
+ try:
700
+ self.event_bus.emit("before_history_clear", agent=self)
701
+ except Exception:
702
+ pass
616
703
  self.model.reset()
617
704
  # 重置后重新设置系统提示词,确保系统约束仍然生效
618
705
  self._setup_system_prompt()
619
706
  # 重置会话
620
707
  self.session.clear_history()
708
+ # 广播清理历史后的事件
709
+ try:
710
+ self.event_bus.emit("after_history_clear", agent=self)
711
+ except Exception:
712
+ pass
621
713
 
622
714
  return formatted_summary
623
715
 
624
716
  def _handle_history_with_file_upload(self) -> str:
625
717
  """使用文件上传方式处理历史"""
626
- return self.file_methodology_manager.handle_history_with_file_upload()
718
+ # 广播清理历史前事件
719
+ try:
720
+ self.event_bus.emit("before_history_clear", agent=self)
721
+ except Exception:
722
+ pass
723
+ result = self.file_methodology_manager.handle_history_with_file_upload()
724
+ # 广播清理历史后的事件
725
+ try:
726
+ self.event_bus.emit("after_history_clear", agent=self)
727
+ except Exception:
728
+ pass
729
+ return result
627
730
 
628
731
  def _format_summary_message(self, summary: str) -> str:
629
732
  """格式化摘要消息"""
@@ -654,30 +757,56 @@ class Agent:
654
757
  2. 对于子Agent: 可能会生成总结(如果启用)
655
758
  3. 使用spinner显示生成状态
656
759
  """
657
- # 收集满意度反馈
658
- satisfaction_feedback = self.task_analyzer.collect_satisfaction_feedback(
659
- auto_completed
660
- )
661
-
662
- if self.use_analysis:
663
- self.task_analyzer.analysis_task(satisfaction_feedback)
664
-
665
- # 当开启强制保存记忆时,在分析步骤之后触发一次记忆保存
666
- if self.force_save_memory:
667
- self.memory_manager.prompt_memory_save()
668
-
760
+ # 事件驱动方式:
761
+ # - TaskAnalyzer 通过订阅 before_summary/task_completed 事件执行分析与满意度收集
762
+ # - MemoryManager 通过订阅 before_history_clear/task_completed 事件执行记忆保存(受 force_save_memory 控制)
763
+ # 为减少耦合,这里不再直接调用上述组件,保持行为由事件触发
669
764
  self._check_and_organize_memory()
670
765
 
766
+ result = "任务完成"
767
+
671
768
  if self.need_summary:
672
769
 
673
770
  self.session.prompt = self.summary_prompt
771
+ # 广播将要生成总结事件
772
+ try:
773
+ self.event_bus.emit(
774
+ "before_summary",
775
+ agent=self,
776
+ prompt=self.session.prompt,
777
+ auto_completed=auto_completed,
778
+ need_summary=self.need_summary,
779
+ )
780
+ except Exception:
781
+ pass
782
+
674
783
  if not self.model:
675
784
  raise RuntimeError("Model not initialized")
676
785
  ret = self.model.chat_until_success(self.session.prompt) # type: ignore
786
+ result = ret
787
+
788
+ # 广播完成总结事件
789
+ try:
790
+ self.event_bus.emit(
791
+ "after_summary",
792
+ agent=self,
793
+ summary=result,
794
+ )
795
+ except Exception:
796
+ pass
677
797
 
678
- return ret
798
+ # 广播任务完成事件(不影响主流程)
799
+ try:
800
+ self.event_bus.emit(
801
+ "task_completed",
802
+ agent=self,
803
+ auto_completed=auto_completed,
804
+ need_summary=self.need_summary,
805
+ )
806
+ except Exception:
807
+ pass
679
808
 
680
- return "任务完成"
809
+ return result
681
810
 
682
811
  def make_default_addon_prompt(self, need_complete: bool) -> str:
683
812
  """生成附加提示。
@@ -686,6 +815,10 @@ class Agent:
686
815
  need_complete: 是否需要完成任务
687
816
 
688
817
  """
818
+ # 优先使用 PromptManager 以保持逻辑集中
819
+ if hasattr(self, "prompt_manager"):
820
+ return self.prompt_manager.build_default_addon_prompt(need_complete)
821
+
689
822
  # 结构化系统指令
690
823
  action_handlers = ", ".join([handler.name() for handler in self.output_handler])
691
824
 
@@ -737,6 +870,17 @@ class Agent:
737
870
  self.session.prompt = f"{user_input}"
738
871
  try:
739
872
  set_agent(self.name, self)
873
+ # 广播任务开始事件(不影响主流程)
874
+ try:
875
+ self.event_bus.emit(
876
+ "task_started",
877
+ agent=self,
878
+ name=self.name,
879
+ description=self.description,
880
+ user_input=self.session.prompt,
881
+ )
882
+ except Exception:
883
+ pass
740
884
  return self._main_loop()
741
885
  except Exception as e:
742
886
  PrettyOutput.print(f"任务失败: {str(e)}", OutputType.ERROR)
@@ -744,81 +888,17 @@ class Agent:
744
888
 
745
889
  def _main_loop(self) -> Any:
746
890
  """主运行循环"""
747
- run_input_handlers = True
891
+ # 委派至独立的运行循环类,保持行为一致
892
+ loop = AgentRunLoop(self)
893
+ return loop.run()
748
894
 
749
- while True:
750
- try:
751
- # 更新输入处理器标志
752
- if self.run_input_handlers_next_turn:
753
- run_input_handlers = True
754
- self.run_input_handlers_next_turn = False
755
-
756
- # 首次运行初始化
757
- if self.first:
758
- self._first_run()
759
-
760
- # 调用模型获取响应
761
- current_response = self._call_model(
762
- self.session.prompt, True, run_input_handlers
763
- )
764
-
765
- self.session.prompt = ""
766
- run_input_handlers = False
767
-
768
- # 处理中断
769
- interrupt_result = self._handle_run_interrupt(current_response)
770
- if interrupt_result:
771
- if isinstance(interrupt_result, tuple):
772
- run_input_handlers, should_continue = interrupt_result
773
- if should_continue:
774
- self.run_input_handlers_next_turn = True
775
- continue
776
- else:
777
- return interrupt_result
778
-
779
- # 处理工具调用
780
- need_return, prompt = self._call_tools(current_response)
781
- if self.session.prompt and prompt:
782
- self.session.prompt += "\n\n" + prompt
783
- else:
784
- self.session.prompt = prompt
785
-
786
- if need_return:
787
- return self.session.prompt
788
-
789
- # 执行回调
790
- if self.after_tool_call_cb:
791
- self.after_tool_call_cb(self)
792
-
793
- # 检查是否需要继续
794
- if self.session.prompt or self.session.addon_prompt:
795
- continue
796
-
797
- # 检查自动完成
798
- if self.auto_complete and ot("!!!COMPLETE!!!") in current_response:
799
- return self._complete_task(auto_completed=True)
800
-
801
- # 获取下一步用户输入
802
- next_action = self._get_next_user_action()
803
- if next_action == "continue":
804
- run_input_handlers = True
805
- continue
806
- elif next_action == "complete":
807
- return self._complete_task(auto_completed=False)
808
-
809
- except Exception as e:
810
- PrettyOutput.print(f"任务失败: {str(e)}", OutputType.ERROR)
811
- return f"Task failed: {str(e)}"
812
-
813
- def _handle_run_interrupt(
814
- self, current_response: str
815
- ) -> Optional[Union[Any, Tuple[bool, bool]]]:
895
+ def _handle_run_interrupt(self, current_response: str) -> Optional[Union[Any, "LoopAction"]]:
816
896
  """处理运行中的中断
817
897
 
818
898
  返回:
819
- None: 无中断,继续执行
820
- Any: 需要返回的结果
821
- Tuple[bool, bool]: (run_input_handlers, should_continue)
899
+ None: 无中断,或中断后允许继续执行当前响应
900
+ Any: 需要返回的最终结果
901
+ LoopAction.SKIP_TURN: 中断后需要跳过当前响应,并立即开始下一次循环
822
902
  """
823
903
  if not get_interrupt():
824
904
  return None
@@ -827,6 +907,16 @@ class Agent:
827
907
  user_input = self._multiline_input(
828
908
  "模型交互期间被中断,请输入用户干预信息:", False
829
909
  )
910
+ # 广播中断事件(包含用户输入,可能为空字符串)
911
+ try:
912
+ self.event_bus.emit(
913
+ "interrupt_triggered",
914
+ agent=self,
915
+ current_response=current_response,
916
+ user_input=user_input,
917
+ )
918
+ except Exception:
919
+ pass
830
920
 
831
921
  self.run_input_handlers_next_turn = True
832
922
 
@@ -840,16 +930,16 @@ class Agent:
840
930
  return None # 继续执行工具调用
841
931
  else:
842
932
  self.session.prompt = f"被用户中断,用户补充信息为:{user_input}\n\n检测到有工具调用,但被用户拒绝执行。请根据用户的补充信息重新考虑下一步操作。"
843
- return (True, True) # run_input_handlers=True, should_continue=True
933
+ return LoopAction.SKIP_TURN # 请求主循环 continue
844
934
  else:
845
935
  self.session.prompt = f"被用户中断,用户补充信息为:{user_input}"
846
- return (True, True) # run_input_handlers=True, should_continue=True
936
+ return LoopAction.SKIP_TURN # 请求主循环 continue
847
937
 
848
- def _get_next_user_action(self) -> str:
938
+ def _get_next_user_action(self) -> Union[str, "LoopAction"]:
849
939
  """获取用户下一步操作
850
940
 
851
941
  返回:
852
- str: "continue""complete"
942
+ LoopAction.CONTINUE 或 LoopAction.COMPLETE(兼容旧字符串值 "continue"/"complete"
853
943
  """
854
944
  user_input = self._multiline_input(
855
945
  f"{self.name}: 请输入,或输入空行来结束当前任务:", False
@@ -857,9 +947,10 @@ class Agent:
857
947
 
858
948
  if user_input:
859
949
  self.session.prompt = user_input
860
- return "continue"
950
+ # 使用显式动作信号,保留返回类型注释以保持兼容
951
+ return LoopAction.CONTINUE # type: ignore[return-value]
861
952
  else:
862
- return "complete"
953
+ return LoopAction.COMPLETE # type: ignore[return-value]
863
954
 
864
955
  def _first_run(self):
865
956
  """首次运行初始化"""
@@ -879,11 +970,20 @@ class Agent:
879
970
 
880
971
  self.first = False
881
972
 
973
+ def _create_temp_model(self, system_prompt: str) -> BasePlatform:
974
+ """创建一个用于执行一次性任务的临时模型实例,以避免污染主会话。"""
975
+ temp_model = PlatformRegistry().create_platform(
976
+ self.model.platform_name() # type: ignore
977
+ )
978
+ if not temp_model:
979
+ raise RuntimeError("创建临时模型失败。")
980
+
981
+ temp_model.set_model_name(self.model.name()) # type: ignore
982
+ temp_model.set_system_prompt(system_prompt)
983
+ return temp_model
984
+
882
985
  def _filter_tools_if_needed(self, task: str):
883
986
  """如果工具数量超过阈值,使用大模型筛选相关工具"""
884
- import re
885
- from jarvis.jarvis_tools.registry import ToolRegistry
886
-
887
987
  tool_registry = self.get_tool_registry()
888
988
  if not isinstance(tool_registry, ToolRegistry):
889
989
  return
@@ -917,19 +1017,21 @@ class Agent:
917
1017
  PrettyOutput.print(
918
1018
  f"工具数量超过{threshold}个,正在使用AI筛选相关工具...", OutputType.INFO
919
1019
  )
920
-
921
- # 使用临时模型实例调用模型,以避免污染历史记录
1020
+ # 广播工具筛选开始事件
922
1021
  try:
923
- from jarvis.jarvis_platform.registry import PlatformRegistry
924
-
925
- temp_model = PlatformRegistry().create_platform(
926
- self.model.platform_name() # type: ignore
1022
+ self.event_bus.emit(
1023
+ "before_tool_filter",
1024
+ agent=self,
1025
+ task=task,
1026
+ total_tools=len(all_tools),
1027
+ threshold=threshold,
927
1028
  )
928
- if not temp_model:
929
- raise RuntimeError("为工具选择创建临时模型失败。")
1029
+ except Exception:
1030
+ pass
930
1031
 
931
- temp_model.set_model_name(self.model.name()) # type: ignore
932
- temp_model.set_system_prompt("你是一个帮助筛选工具的助手。")
1032
+ # 使用临时模型实例调用模型,以避免污染历史记录
1033
+ try:
1034
+ temp_model = self._create_temp_model("你是一个帮助筛选工具的助手。")
933
1035
  selected_tools_str = temp_model.chat_until_success(
934
1036
  selection_prompt
935
1037
  ) # type: ignore
@@ -954,10 +1056,34 @@ class Agent:
954
1056
  f"已筛选出 {len(selected_tool_names)} 个相关工具: {', '.join(selected_tool_names)}",
955
1057
  OutputType.SUCCESS,
956
1058
  )
1059
+ # 广播工具筛选事件
1060
+ try:
1061
+ self.event_bus.emit(
1062
+ "tool_filtered",
1063
+ agent=self,
1064
+ task=task,
1065
+ selected_tools=selected_tool_names,
1066
+ total_tools=len(all_tools),
1067
+ threshold=threshold,
1068
+ )
1069
+ except Exception:
1070
+ pass
957
1071
  else:
958
1072
  PrettyOutput.print(
959
1073
  "AI 未能筛选出任何相关工具,将使用所有工具。", OutputType.WARNING
960
1074
  )
1075
+ # 广播工具筛选事件(无筛选结果)
1076
+ try:
1077
+ self.event_bus.emit(
1078
+ "tool_filtered",
1079
+ agent=self,
1080
+ task=task,
1081
+ selected_tools=[],
1082
+ total_tools=len(all_tools),
1083
+ threshold=threshold,
1084
+ )
1085
+ except Exception:
1086
+ pass
961
1087
 
962
1088
  except Exception as e:
963
1089
  PrettyOutput.print(