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
@@ -25,10 +25,14 @@ class AgentManager:
25
25
  model_group: Optional[str] = None,
26
26
  tool_group: Optional[str] = None,
27
27
  restore_session: bool = False,
28
+ use_methodology: Optional[bool] = None,
29
+ use_analysis: Optional[bool] = None,
28
30
  ):
29
31
  self.model_group = model_group
30
32
  self.tool_group = tool_group
31
33
  self.restore_session = restore_session
34
+ self.use_methodology = use_methodology
35
+ self.use_analysis = use_analysis
32
36
  self.agent: Optional[Agent] = None
33
37
 
34
38
  def initialize(self) -> Agent:
@@ -45,6 +49,8 @@ class AgentManager:
45
49
  input_handler=[shell_input_handler, builtin_input_handler],
46
50
  output_handler=[ToolRegistry()], # type: ignore
47
51
  need_summary=False,
52
+ use_methodology=self.use_methodology,
53
+ use_analysis=self.use_analysis,
48
54
  )
49
55
 
50
56
  # 尝试恢复会话
@@ -0,0 +1,92 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ AgentConfig: 聚合 Agent 的初始化配置并提供默认值解析。
4
+
5
+ 目标(阶段一,最小变更):
6
+ - 提供独立的配置承载类,封装 __init__ 中的配置项
7
+ - 支持从全局配置与上下文推导默认值
8
+ - 暂不强制替换 Agent 的现有参数与流程,后续逐步接入
9
+ """
10
+ from dataclasses import dataclass, field
11
+ from typing import List, Optional
12
+
13
+ from jarvis.jarvis_agent.prompts import DEFAULT_SUMMARY_PROMPT
14
+ from jarvis.jarvis_utils.config import (
15
+ get_max_token_count,
16
+ is_execute_tool_confirm,
17
+ is_force_save_memory,
18
+ is_use_analysis,
19
+ is_use_methodology,
20
+ )
21
+
22
+
23
+ @dataclass
24
+ class AgentConfig:
25
+ # 核心身份与系统参数
26
+ system_prompt: str
27
+ name: str = "Jarvis"
28
+ description: str = ""
29
+ model_group: Optional[str] = None
30
+
31
+ # 运行行为
32
+ auto_complete: bool = False
33
+ need_summary: bool = True
34
+
35
+ # 可选配置(None 表示使用默认策略解析)
36
+ summary_prompt: Optional[str] = None
37
+ execute_tool_confirm: Optional[bool] = None
38
+ use_methodology: Optional[bool] = None
39
+ use_analysis: Optional[bool] = None
40
+ force_save_memory: Optional[bool] = None
41
+ files: Optional[List[str]] = field(default_factory=list)
42
+ max_token_count: Optional[int] = None
43
+
44
+ def resolve_defaults(self) -> "AgentConfig":
45
+ """
46
+ 解析并填充默认值,返回新的 AgentConfig 实例,不修改原对象。
47
+ 策略与 Agent._init_config 中的逻辑保持一致,确保兼容。
48
+ """
49
+ # 复制当前实例的浅拷贝数据
50
+ cfg = AgentConfig(
51
+ system_prompt=self.system_prompt,
52
+ name=self.name,
53
+ description=self.description,
54
+ model_group=self.model_group,
55
+ auto_complete=self.auto_complete,
56
+ need_summary=self.need_summary,
57
+ summary_prompt=self.summary_prompt,
58
+ execute_tool_confirm=self.execute_tool_confirm,
59
+ use_methodology=self.use_methodology,
60
+ use_analysis=self.use_analysis,
61
+ force_save_memory=self.force_save_memory,
62
+ files=list(self.files or []),
63
+ max_token_count=self.max_token_count,
64
+ )
65
+
66
+ # use_methodology: 若存在上传文件则禁用;否则按照外部传入或全局默认
67
+ if cfg.files:
68
+ cfg.use_methodology = False
69
+ elif cfg.use_methodology is None:
70
+ cfg.use_methodology = is_use_methodology()
71
+
72
+ # use_analysis
73
+ if cfg.use_analysis is None:
74
+ cfg.use_analysis = is_use_analysis()
75
+
76
+ # execute_tool_confirm
77
+ if cfg.execute_tool_confirm is None:
78
+ cfg.execute_tool_confirm = is_execute_tool_confirm()
79
+
80
+ # summary_prompt
81
+ if cfg.summary_prompt is None:
82
+ cfg.summary_prompt = DEFAULT_SUMMARY_PROMPT
83
+
84
+ # max_token_count
85
+ if cfg.max_token_count is None:
86
+ cfg.max_token_count = get_max_token_count(cfg.model_group)
87
+
88
+ # force_save_memory
89
+ if cfg.force_save_memory is None:
90
+ cfg.force_save_memory = is_force_save_memory()
91
+
92
+ return cfg
@@ -51,7 +51,7 @@ class ConfigEditor:
51
51
  raise typer.Exit(code=1)
52
52
  else:
53
53
  PrettyOutput.print(
54
- f"No suitable editor found. Please install one of: vim, nano, emacs, code",
54
+ "No suitable editor found. Please install one of: vim, nano, emacs, code",
55
55
  OutputType.ERROR,
56
56
  )
57
57
  raise typer.Exit(code=1)
@@ -0,0 +1,48 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 事件总线(EventBus)
4
+
5
+ 目标(阶段一,最小变更):
6
+ - 提供简单可靠的发布/订阅机制
7
+ - 回调异常隔离,避免影响主流程
8
+ - 不引入额外依赖,便于在 Agent 中渐进集成
9
+ """
10
+ from collections import defaultdict
11
+ from typing import Callable, DefaultDict, Dict, List
12
+
13
+
14
+
15
+ class EventBus:
16
+ """
17
+ 简单的同步事件总线。
18
+ - subscribe(event, callback): 订阅事件
19
+ - emit(event, **kwargs): 广播事件
20
+ - unsubscribe(event, callback): 取消订阅
21
+ """
22
+
23
+ def __init__(self) -> None:
24
+ self._listeners: DefaultDict[str, List[Callable[..., None]]] = defaultdict(list)
25
+
26
+ def subscribe(self, event: str, callback: Callable[..., None]) -> None:
27
+ if not callable(callback):
28
+ raise TypeError("callback must be callable")
29
+ self._listeners[event].append(callback)
30
+
31
+ def unsubscribe(self, event: str, callback: Callable[..., None]) -> None:
32
+ if event not in self._listeners:
33
+ return
34
+ try:
35
+ self._listeners[event].remove(callback)
36
+ except ValueError:
37
+ pass
38
+
39
+ def emit(self, event: str, **payload: Dict) -> None:
40
+ """
41
+ 广播事件。回调中的异常将被捕获并忽略,以保证主流程稳定。
42
+ """
43
+ for cb in list(self._listeners.get(event, [])):
44
+ try:
45
+ cb(**payload)
46
+ except Exception:
47
+ # 避免回调异常中断主流程
48
+ continue
@@ -5,7 +5,6 @@
5
5
  """
6
6
  import os
7
7
  import tempfile
8
- from typing import List, Optional
9
8
 
10
9
  from jarvis.jarvis_utils.methodology import load_methodology, upload_methodology
11
10
  from jarvis.jarvis_utils.output import OutputType, PrettyOutput
@@ -46,7 +45,6 @@ class FileMethodologyManager:
46
45
  self._load_local_methodology()
47
46
  else:
48
47
  # 上传成功
49
- from jarvis.jarvis_agent.memory_manager import MemoryManager
50
48
 
51
49
  if self.agent.files:
52
50
  self.agent.session.prompt = f"{self.agent.session.prompt}\n\n上传的文件包含历史对话信息和方法论文件,可以从中获取一些经验信息。"
@@ -75,7 +73,7 @@ class FileMethodologyManager:
75
73
 
76
74
  from jarvis.jarvis_agent.memory_manager import MemoryManager
77
75
 
78
- memory_manager = MemoryManager(self.agent)
76
+ MemoryManager(self.agent)
79
77
  methodology = load_methodology(
80
78
  msg,
81
79
  self.agent.get_tool_registry(),
@@ -19,8 +19,8 @@ from jarvis.jarvis_utils.config import (
19
19
  )
20
20
  import jarvis.jarvis_utils.utils as jutils
21
21
  from jarvis.jarvis_utils.input import user_confirm, get_single_line_input
22
+ from jarvis.jarvis_utils.fzf import fzf_select
22
23
  import os
23
- import sys
24
24
  import subprocess
25
25
  from pathlib import Path
26
26
  import yaml # type: ignore
@@ -403,47 +403,80 @@ def handle_builtin_config_selector(
403
403
 
404
404
  Console().print(table)
405
405
 
406
- choice = get_single_line_input(
407
- "选择要启动的配置编号,直接回车使用默认通用代理(jvs): ", default=""
406
+ # Try to use fzf for selection if available (include No. to support number-based filtering)
407
+ fzf_options = [
408
+ f"{idx:>3} | {opt['category']:<12} | {opt['name']:<30} | {opt.get('desc', '')}"
409
+ for idx, opt in enumerate(options, 1)
410
+ ]
411
+ selected_str = fzf_select(
412
+ fzf_options, prompt="选择要启动的配置编号 (ESC跳过) > "
408
413
  )
409
414
 
410
- if choice.strip():
415
+ choice_index = -1
416
+ if selected_str:
417
+ # Try to parse leading number before first '|'
411
418
  try:
412
- index = int(choice.strip())
413
- if 1 <= index <= len(options):
414
- sel = options[index - 1]
415
- args: list[str] = []
416
-
417
- if sel["category"] == "agent":
418
- # jarvis-agent 支持 -f/--config(全局配置)与 -c/--agent-definition
419
- args = [str(sel["cmd"]), "-c", str(sel["file"])]
420
- if model_group:
421
- args += ["-g", str(model_group)]
422
- if config_file:
423
- args += ["-f", str(config_file)]
424
- if task:
425
- args += ["--task", str(task)]
426
-
427
- elif sel["category"] == "multi_agent":
428
- # jarvis-multi-agent 需要 -c/--config,用户输入通过 -i/--input 传递
429
- args = [str(sel["cmd"]), "-c", str(sel["file"])]
430
- if task:
431
- args += ["-i", str(task)]
432
-
433
- elif sel["category"] == "roles":
434
- # jarvis-platform-manager role 子命令,支持 -c/-t/-g
435
- args = [str(sel["cmd"]), "role", "-c", str(sel["file"])]
436
- if model_group:
437
- args += ["-g", str(model_group)]
438
-
439
- if args:
440
- PrettyOutput.print(
441
- f"正在启动: {' '.join(args)}", OutputType.INFO
442
- )
443
- os.execvp(args[0], args)
419
+ num_part = selected_str.split("|", 1)[0].strip()
420
+ selected_index = int(num_part)
421
+ if 1 <= selected_index <= len(options):
422
+ choice_index = selected_index - 1
423
+ except Exception:
424
+ # Fallback to equality matching if parsing fails
425
+ for i, fzf_opt in enumerate(fzf_options):
426
+ if fzf_opt == selected_str:
427
+ choice_index = i
428
+ break
429
+ else:
430
+ # Fallback to manual input if fzf is not used or available
431
+ choice = get_single_line_input(
432
+ "选择要启动的配置编号,直接回车使用默认通用代理(jvs): ", default=""
433
+ )
434
+ if choice.strip():
435
+ try:
436
+ selected_index = int(choice.strip())
437
+ if 1 <= selected_index <= len(options):
438
+ choice_index = selected_index - 1
439
+ except ValueError:
440
+ pass # Invalid input
441
+
442
+ if choice_index != -1:
443
+ try:
444
+ sel = options[choice_index]
445
+ args: list[str] = []
446
+
447
+ if sel["category"] == "agent":
448
+ # jarvis-agent 支持 -f/--config(全局配置)与 -c/--agent-definition
449
+ args = [str(sel["cmd"]), "-c", str(sel["file"])]
450
+ if model_group:
451
+ args += ["-g", str(model_group)]
452
+ if config_file:
453
+ args += ["-f", str(config_file)]
454
+ if task:
455
+ args += ["--task", str(task)]
456
+
457
+ elif sel["category"] == "multi_agent":
458
+ # jarvis-multi-agent 需要 -c/--config,用户输入通过 -i/--input 传递
459
+ args = [str(sel["cmd"]), "-c", str(sel["file"])]
460
+ if task:
461
+ args += ["-i", str(task)]
462
+
463
+ elif sel["category"] == "roles":
464
+ # jarvis-platform-manager role 子命令,支持 -c/-t/-g
465
+ args = [str(sel["cmd"]), "role", "-c", str(sel["file"])]
466
+ if model_group:
467
+ args += ["-g", str(model_group)]
468
+
469
+ if args:
470
+ PrettyOutput.print(
471
+ f"正在启动: {' '.join(args)}", OutputType.INFO
472
+ )
473
+ os.execvp(args[0], args)
444
474
  except Exception:
445
475
  # 任何异常都不影响默认流程
446
476
  pass
477
+ else:
478
+ # User pressed Enter or provided invalid input
479
+ pass
447
480
  except Exception:
448
481
  # 静默忽略内置配置扫描错误,不影响主流程
449
482
  pass
@@ -482,6 +515,12 @@ def run_cli(
482
515
  "--interactive-config",
483
516
  help="启动交互式配置向导(基于当前配置补充设置)",
484
517
  ),
518
+ disable_methodology_analysis: bool = typer.Option(
519
+ False,
520
+ "-D",
521
+ "--disable-methodology-analysis",
522
+ help="禁用方法论和任务分析(覆盖配置文件设置)",
523
+ ),
485
524
  ) -> None:
486
525
  """Jarvis AI assistant command-line interface."""
487
526
  if ctx.invoked_subcommand is not None:
@@ -528,6 +567,8 @@ def run_cli(
528
567
  model_group=model_group,
529
568
  tool_group=tool_group,
530
569
  restore_session=restore_session,
570
+ use_methodology=False if disable_methodology_analysis else None,
571
+ use_analysis=False if disable_methodology_analysis else None,
531
572
  )
532
573
  agent_manager.initialize()
533
574
  agent_manager.run_task(task)
@@ -3,7 +3,6 @@
3
3
  记忆管理器模块
4
4
  负责处理Agent的记忆保存和检索功能
5
5
  """
6
- from typing import Optional, Dict, List, Any
7
6
 
8
7
  from jarvis.jarvis_utils.globals import get_all_memory_tags
9
8
  from jarvis.jarvis_utils.output import OutputType, PrettyOutput
@@ -20,6 +19,13 @@ class MemoryManager:
20
19
  agent: Agent实例
21
20
  """
22
21
  self.agent = agent
22
+ # 本轮任务是否已进行过记忆保存提示/处理的标记,用于事件去重
23
+ self._memory_prompted = False
24
+ # 订阅 Agent 事件(旁路集成,失败不影响主流程)
25
+ try:
26
+ self._subscribe_events()
27
+ except Exception:
28
+ pass
23
29
 
24
30
  def prepare_memory_tags_prompt(self) -> str:
25
31
  """准备记忆标签提示"""
@@ -88,13 +94,27 @@ class MemoryManager:
88
94
 
89
95
  # 处理记忆保存
90
96
  try:
97
+ # 清空本轮执行标记,便于准确判断是否调用了 save_memory
98
+ try:
99
+ self.agent.set_user_data("__last_executed_tool__", "")
100
+ self.agent.set_user_data("__executed_tools__", [])
101
+ except Exception:
102
+ pass
103
+
91
104
  response = self.agent.model.chat_until_success(prompt) # type: ignore
92
105
 
93
106
  # 执行工具调用(如果有)
94
107
  need_return, result = self.agent._call_tools(response)
95
108
 
96
- # 根据响应判断是否保存了记忆
97
- if "save_memory" in response:
109
+ # 根据实际执行的工具判断是否保存了记忆
110
+ saved = False
111
+ try:
112
+ last_tool = self.agent.get_user_data("__last_executed_tool__")
113
+ saved = last_tool == "save_memory"
114
+ except Exception:
115
+ saved = False
116
+
117
+ if saved:
98
118
  PrettyOutput.print(
99
119
  "已自动保存有价值的信息到记忆系统", OutputType.SUCCESS
100
120
  )
@@ -103,6 +123,13 @@ class MemoryManager:
103
123
 
104
124
  except Exception as e:
105
125
  PrettyOutput.print(f"记忆分析失败: {str(e)}", OutputType.ERROR)
126
+ finally:
127
+ # 设置记忆提示完成标记,避免事件触发造成重复处理
128
+ self._memory_prompted = True
129
+ try:
130
+ self.agent.set_user_data("__memory_save_prompted__", True)
131
+ except Exception:
132
+ pass
106
133
 
107
134
  def add_memory_prompts_to_addon(self, addon_prompt: str, tool_registry) -> str:
108
135
  """在附加提示中添加记忆相关提示"""
@@ -125,3 +152,43 @@ class MemoryManager:
125
152
  memory_prompts += "\n - 如果需要获取上下文或寻找解决方案,请调用retrieve_memory工具检索相关记忆"
126
153
 
127
154
  return memory_prompts
155
+
156
+ # -----------------------
157
+ # 事件订阅与处理(旁路)
158
+ # -----------------------
159
+ def _subscribe_events(self) -> None:
160
+ bus = self.agent.get_event_bus() # type: ignore[attr-defined]
161
+ # 任务开始时重置去重标记
162
+ bus.subscribe("task_started", self._on_task_started)
163
+ # 在清理历史前尝试保存记忆(若开启强制保存且尚未处理)
164
+ bus.subscribe("before_history_clear", self._ensure_memory_prompt)
165
+ # 任务完成时作为兜底再尝试一次
166
+ bus.subscribe("task_completed", self._ensure_memory_prompt)
167
+
168
+ def _on_task_started(self, **payload) -> None:
169
+ self._memory_prompted = False
170
+ try:
171
+ self.agent.set_user_data("__memory_save_prompted__", False)
172
+ except Exception:
173
+ pass
174
+
175
+ def _ensure_memory_prompt(self, **payload) -> None:
176
+ # 仅在开启强制保存记忆时启用
177
+ if not getattr(self.agent, "force_save_memory", False):
178
+ return
179
+ # 避免在同一任务内重复提示/处理
180
+ if self._memory_prompted:
181
+ return
182
+ try:
183
+ already = bool(self.agent.get_user_data("__memory_save_prompted__"))
184
+ if already:
185
+ self._memory_prompted = True
186
+ return
187
+ except Exception:
188
+ pass
189
+ # 静默执行保存逻辑,失败不影响主流程
190
+ try:
191
+ self.prompt_memory_save()
192
+ except Exception:
193
+ # 忽略异常,保持主流程稳定
194
+ self._memory_prompted = True
@@ -0,0 +1,82 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ PromptManager: 统一管理 Agent 的系统提示词与附加提示词的构建逻辑。
4
+
5
+ 设计目标(阶段一,最小变更):
6
+ - 提供独立的提示构建类,不改变现有行为
7
+ - 先行落地构建逻辑,后续在 Agent 中逐步委派使用
8
+ - 保持与现有工具/记忆系统兼容
9
+ """
10
+ from typing import TYPE_CHECKING
11
+
12
+ from jarvis.jarvis_tools.registry import ToolRegistry
13
+ from jarvis.jarvis_utils.tag import ot
14
+
15
+ if TYPE_CHECKING:
16
+ # 避免运行时循环依赖,仅用于类型标注
17
+ from . import Agent # noqa: F401
18
+
19
+
20
+
21
+ class PromptManager:
22
+ """
23
+ 提示管理器:负责构建系统提示与默认附加提示。
24
+ 注意:该类不直接访问模型,只负责拼装字符串。
25
+ """
26
+
27
+ def __init__(self, agent: "Agent"):
28
+ self.agent = agent
29
+
30
+ # ----------------------------
31
+ # 系统提示词构建
32
+ # ----------------------------
33
+ def build_system_prompt(self) -> str:
34
+ """
35
+ 构建系统提示词,复用现有的工具使用提示生成逻辑,保持行为一致。
36
+ """
37
+ action_prompt = self.agent.get_tool_usage_prompt()
38
+ return f"""
39
+ {self.agent.system_prompt}
40
+
41
+ {action_prompt}
42
+ """
43
+
44
+ # ----------------------------
45
+ # 附加提示词构建
46
+ # ----------------------------
47
+ def build_default_addon_prompt(self, need_complete: bool) -> str:
48
+ """
49
+ 构建默认附加提示词(与 Agent.make_default_addon_prompt 行为保持一致)。
50
+ 仅进行字符串拼装,不操作会话状态。
51
+ """
52
+ # 结构化系统指令
53
+ action_handlers = ", ".join([handler.name() for handler in self.agent.output_handler])
54
+
55
+ # 任务完成提示
56
+ complete_prompt = (
57
+ f"- 输出{ot('!!!COMPLETE!!!')}"
58
+ if need_complete and self.agent.auto_complete
59
+ else ""
60
+ )
61
+
62
+ # 工具与记忆相关提示
63
+ tool_registry = self.agent.get_tool_registry()
64
+ memory_prompts = self.agent.memory_manager.add_memory_prompts_to_addon(
65
+ "", tool_registry if isinstance(tool_registry, ToolRegistry) else None
66
+ )
67
+
68
+ addon_prompt = f"""
69
+ <system_prompt>
70
+ 请判断是否已经完成任务,如果已经完成:
71
+ - 直接输出完成原因,不需要再有新的操作,不要输出{ot("TOOL_CALL")}标签
72
+ {complete_prompt}
73
+ 如果没有完成,请进行下一步操作:
74
+ - 仅包含一个操作
75
+ - 如果信息不明确,请请求用户补充
76
+ - 如果执行过程中连续失败5次,请使用ask_user询问用户操作
77
+ - 操作列表:{action_handlers}{memory_prompts}
78
+ </system_prompt>
79
+
80
+ 请继续。
81
+ """
82
+ return addon_prompt
@@ -0,0 +1,130 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ AgentRunLoop: 承载 Agent 的主运行循环逻辑。
4
+
5
+ 阶段一目标(最小变更):
6
+ - 复制现有 _main_loop 逻辑到独立类,使用传入的 agent 实例进行委派调用
7
+ - 暂不变更外部调用入口,后续在 Agent._main_loop 中委派到该类
8
+ - 保持与现有异常处理、工具调用、用户交互完全一致
9
+ """
10
+ from enum import Enum
11
+ from typing import Any, TYPE_CHECKING
12
+
13
+ from jarvis.jarvis_utils.output import OutputType, PrettyOutput
14
+ from jarvis.jarvis_utils.tag import ot
15
+
16
+ if TYPE_CHECKING:
17
+ # 仅用于类型标注,避免运行时循环依赖
18
+ from . import Agent # noqa: F401
19
+
20
+
21
+ class AgentRunLoop:
22
+ def __init__(self, agent: "Agent") -> None:
23
+ self.agent = agent
24
+
25
+ def run(self) -> Any:
26
+ """主运行循环(委派到传入的 agent 实例的方法与属性)"""
27
+ run_input_handlers = True
28
+
29
+ while True:
30
+ try:
31
+ ag = self.agent
32
+
33
+ # 更新输入处理器标志
34
+ if ag.run_input_handlers_next_turn:
35
+ run_input_handlers = True
36
+ ag.run_input_handlers_next_turn = False
37
+
38
+ # 首次运行初始化
39
+ if ag.first:
40
+ ag._first_run()
41
+
42
+ # 调用模型获取响应
43
+ current_response = ag._call_model(
44
+ ag.session.prompt, True, run_input_handlers
45
+ )
46
+
47
+ ag.session.prompt = ""
48
+ run_input_handlers = False
49
+
50
+ # 处理中断
51
+ interrupt_result = ag._handle_run_interrupt(current_response)
52
+ if (
53
+ isinstance(interrupt_result, Enum)
54
+ and getattr(interrupt_result, "value", None) == "skip_turn"
55
+ ):
56
+ # 中断处理器请求跳过本轮剩余部分,直接开始下一次循环
57
+ continue
58
+ elif interrupt_result is not None and not isinstance(interrupt_result, Enum):
59
+ # 中断处理器返回了最终结果,任务结束
60
+ return interrupt_result
61
+
62
+ # 处理工具调用
63
+ # 广播工具调用前事件(不影响主流程)
64
+ try:
65
+ ag.event_bus.emit(
66
+ "before_tool_call",
67
+ agent=ag,
68
+ current_response=current_response,
69
+ )
70
+ except Exception:
71
+ pass
72
+ need_return, tool_prompt = ag._call_tools(current_response)
73
+
74
+ # 将上一个提示和工具提示安全地拼接起来
75
+ prompt_parts = []
76
+ if ag.session.prompt:
77
+ prompt_parts.append(ag.session.prompt)
78
+ if tool_prompt:
79
+ prompt_parts.append(tool_prompt)
80
+ ag.session.prompt = "\n\n".join(prompt_parts)
81
+
82
+ if need_return:
83
+ return ag.session.prompt
84
+
85
+ # 执行回调
86
+ if ag.after_tool_call_cb:
87
+ ag.after_tool_call_cb(ag)
88
+ # 广播工具调用后的事件(不影响主流程)
89
+ try:
90
+ ag.event_bus.emit(
91
+ "after_tool_call",
92
+ agent=ag,
93
+ current_response=current_response,
94
+ need_return=need_return,
95
+ tool_prompt=tool_prompt,
96
+ )
97
+ except Exception:
98
+ pass
99
+
100
+ # 检查是否需要继续
101
+ if ag.session.prompt or ag.session.addon_prompt:
102
+ continue
103
+
104
+ # 检查自动完成
105
+ if ag.auto_complete and ot("!!!COMPLETE!!!") in current_response:
106
+ return ag._complete_task(auto_completed=True)
107
+
108
+ # 获取下一步用户输入
109
+ next_action = ag._get_next_user_action()
110
+ if (
111
+ next_action == "continue"
112
+ or (
113
+ isinstance(next_action, Enum)
114
+ and getattr(next_action, "value", None) == "continue"
115
+ )
116
+ ):
117
+ run_input_handlers = True
118
+ continue
119
+ elif (
120
+ next_action == "complete"
121
+ or (
122
+ isinstance(next_action, Enum)
123
+ and getattr(next_action, "value", None) == "complete"
124
+ )
125
+ ):
126
+ return ag._complete_task(auto_completed=False)
127
+
128
+ except Exception as e:
129
+ PrettyOutput.print(f"任务失败: {str(e)}", OutputType.ERROR)
130
+ return f"Task failed: {str(e)}"
@@ -28,7 +28,7 @@ def shell_input_handler(user_input: str, agent: Any) -> Tuple[str, bool]:
28
28
  # If any line contains the no-confirm marker, skip the pre-execution confirmation
29
29
  no_confirm = any(marker in c for c in cmdline)
30
30
 
31
- if no_confirm or user_confirm(f"是否要执行以上shell脚本?", default=True):
31
+ if no_confirm or user_confirm("是否要执行以上shell脚本?", default=True):
32
32
  from jarvis.jarvis_tools.registry import ToolRegistry
33
33
 
34
34
  output = ToolRegistry().handle_tool_calls(