jarvis-ai-assistant 0.4.2__py3-none-any.whl → 0.5.1__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 (30) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +117 -6
  3. jarvis/jarvis_agent/jarvis.py +6 -0
  4. jarvis/jarvis_agent/share_manager.py +8 -1
  5. jarvis/jarvis_agent/task_planner.py +218 -0
  6. jarvis/jarvis_code_agent/code_agent.py +99 -3
  7. jarvis/jarvis_code_analysis/code_review.py +483 -568
  8. jarvis/jarvis_data/config_schema.json +8 -3
  9. jarvis/jarvis_sec/README.md +180 -0
  10. jarvis/jarvis_sec/__init__.py +674 -0
  11. jarvis/jarvis_sec/checkers/__init__.py +33 -0
  12. jarvis/jarvis_sec/checkers/c_checker.py +1269 -0
  13. jarvis/jarvis_sec/checkers/rust_checker.py +367 -0
  14. jarvis/jarvis_sec/cli.py +110 -0
  15. jarvis/jarvis_sec/prompts.py +324 -0
  16. jarvis/jarvis_sec/report.py +260 -0
  17. jarvis/jarvis_sec/types.py +20 -0
  18. jarvis/jarvis_sec/workflow.py +513 -0
  19. jarvis/jarvis_tools/registry.py +20 -14
  20. jarvis/jarvis_tools/sub_agent.py +4 -3
  21. jarvis/jarvis_tools/sub_code_agent.py +3 -3
  22. jarvis/jarvis_utils/config.py +14 -2
  23. jarvis/jarvis_utils/methodology.py +25 -19
  24. jarvis/jarvis_utils/utils.py +193 -2
  25. {jarvis_ai_assistant-0.4.2.dist-info → jarvis_ai_assistant-0.5.1.dist-info}/METADATA +1 -1
  26. {jarvis_ai_assistant-0.4.2.dist-info → jarvis_ai_assistant-0.5.1.dist-info}/RECORD +30 -19
  27. {jarvis_ai_assistant-0.4.2.dist-info → jarvis_ai_assistant-0.5.1.dist-info}/entry_points.txt +2 -0
  28. {jarvis_ai_assistant-0.4.2.dist-info → jarvis_ai_assistant-0.5.1.dist-info}/WHEEL +0 -0
  29. {jarvis_ai_assistant-0.4.2.dist-info → jarvis_ai_assistant-0.5.1.dist-info}/licenses/LICENSE +0 -0
  30. {jarvis_ai_assistant-0.4.2.dist-info → jarvis_ai_assistant-0.5.1.dist-info}/top_level.txt +0 -0
jarvis/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """Jarvis AI Assistant"""
3
3
 
4
- __version__ = "0.4.2"
4
+ __version__ = "0.5.1"
@@ -25,6 +25,7 @@ from jarvis.jarvis_agent.tool_executor import execute_tool_call
25
25
  from jarvis.jarvis_agent.memory_manager import MemoryManager
26
26
  from jarvis.jarvis_memory_organizer.memory_organizer import MemoryOrganizer
27
27
  from jarvis.jarvis_agent.task_analyzer import TaskAnalyzer
28
+ from jarvis.jarvis_agent.task_planner import TaskPlanner
28
29
  from jarvis.jarvis_agent.file_methodology_manager import FileMethodologyManager
29
30
  from jarvis.jarvis_agent.prompts import (
30
31
  DEFAULT_SUMMARY_PROMPT,
@@ -75,8 +76,7 @@ from jarvis.jarvis_utils.config import (
75
76
  is_use_methodology,
76
77
  get_tool_filter_threshold,
77
78
  get_after_tool_call_cb_dirs,
78
-
79
-
79
+ get_plan_max_depth,
80
80
  )
81
81
  from jarvis.jarvis_utils.embedding import get_context_token_count
82
82
  from jarvis.jarvis_utils.globals import (
@@ -249,8 +249,23 @@ class Agent:
249
249
  def clear_history(self):
250
250
  """
251
251
  Clears the current conversation history by delegating to the session manager.
252
+ Emits BEFORE_HISTORY_CLEAR/AFTER_HISTORY_CLEAR and reapplies system prompt to preserve constraints.
252
253
  """
254
+ # 广播清理历史前事件(不影响主流程)
255
+ try:
256
+ self.event_bus.emit(BEFORE_HISTORY_CLEAR, agent=self)
257
+ except Exception:
258
+ pass
259
+
260
+ # 清理会话历史并重置模型状态
253
261
  self.session.clear_history()
262
+
263
+ # 重置后重新设置系统提示词,确保系统约束仍然生效
264
+ try:
265
+ self._setup_system_prompt()
266
+ except Exception:
267
+ pass
268
+
254
269
  # 广播清理历史后的事件
255
270
  try:
256
271
  self.event_bus.emit(AFTER_HISTORY_CLEAR, agent=self)
@@ -270,6 +285,21 @@ class Agent:
270
285
  """获取工具使用提示"""
271
286
  return build_action_prompt(self.output_handler) # type: ignore
272
287
 
288
+ def __new__(cls, *args, **kwargs):
289
+ if kwargs.get("agent_type") == "code":
290
+ try:
291
+ from jarvis.jarvis_code_agent.code_agent import CodeAgent
292
+ except ImportError as e:
293
+ raise RuntimeError(
294
+ "CodeAgent could not be imported. Please ensure jarvis_code_agent is installed correctly."
295
+ ) from e
296
+
297
+ # 移除 agent_type 避免无限循环,并传递所有其他参数
298
+ kwargs.pop("agent_type", None)
299
+ return CodeAgent(**kwargs)
300
+ else:
301
+ return super().__new__(cls)
302
+
273
303
  def __init__(
274
304
  self,
275
305
  system_prompt: str,
@@ -291,6 +321,10 @@ class Agent:
291
321
  confirm_callback: Optional[Callable[[str, bool], bool]] = None,
292
322
  non_interactive: Optional[bool] = None,
293
323
  in_multi_agent: Optional[bool] = None,
324
+ plan: bool = False,
325
+ plan_max_depth: Optional[int] = None,
326
+ plan_depth: int = 0,
327
+ agent_type: str = "normal",
294
328
  **kwargs,
295
329
  ):
296
330
  """初始化Jarvis Agent实例
@@ -310,6 +344,9 @@ class Agent:
310
344
  force_save_memory: 是否强制保存记忆
311
345
  confirm_callback: 用户确认回调函数,签名为 (tip: str, default: bool) -> bool;默认使用CLI的user_confirm
312
346
  non_interactive: 是否以非交互模式运行(优先级最高,覆盖环境变量与配置)
347
+ plan: 是否启用任务规划与子任务拆分(默认 False;启用后在进入主循环前评估是否需要将任务拆分为 <SUB_TASK> 列表,逐一由子Agent执行并汇总结果)
348
+ plan_max_depth: 任务规划的最大层数(默认3,可通过配置 JARVIS_PLAN_MAX_DEPTH 或入参覆盖)
349
+ plan_depth: 当前规划层数(内部用于递归控制,子Agent会在父基础上+1)
313
350
  """
314
351
  # 基础属性初始化(仅根据入参设置原始值;实际生效的默认回退在 _init_config 中统一解析)
315
352
  # 标识与描述
@@ -333,6 +370,18 @@ class Agent:
333
370
  self.non_interactive = non_interactive
334
371
  # 多智能体运行标志:用于控制非交互模式下的自动完成行为
335
372
  self.in_multi_agent = bool(in_multi_agent)
373
+ self.plan = bool(plan)
374
+ # 规划深度与上限
375
+ try:
376
+ self.plan_max_depth = (
377
+ int(plan_max_depth) if plan_max_depth is not None else int(get_plan_max_depth())
378
+ )
379
+ except Exception:
380
+ self.plan_max_depth = 2
381
+ try:
382
+ self.plan_depth = int(plan_depth)
383
+ except Exception:
384
+ self.plan_depth = 0
336
385
  # 运行时状态
337
386
  self.first = True
338
387
  self.run_input_handlers_next_turn = False
@@ -414,6 +463,8 @@ class Agent:
414
463
  self.task_analyzer = TaskAnalyzer(self)
415
464
  self.file_methodology_manager = FileMethodologyManager(self)
416
465
  self.prompt_manager = PromptManager(self)
466
+ # 任务规划器:封装规划与子任务调度逻辑
467
+ self.task_planner = TaskPlanner(self, plan_depth=self.plan_depth, plan_max_depth=self.plan_max_depth)
417
468
 
418
469
  # 设置系统提示词
419
470
  self._setup_system_prompt()
@@ -786,9 +837,14 @@ class Agent:
786
837
  try:
787
838
  if not self.model:
788
839
  raise RuntimeError("Model not initialized")
789
- summary = self.model.chat_until_success(
790
- self.session.prompt + "\n" + SUMMARY_REQUEST_PROMPT
791
- ) # type: ignore
840
+ # 优先使用外部传入的 summary_prompt;如为空则回退到默认的会话摘要请求
841
+ safe_summary_prompt = self.summary_prompt or ""
842
+ if isinstance(safe_summary_prompt, str) and safe_summary_prompt.strip() != "":
843
+ prompt_to_use = safe_summary_prompt
844
+ else:
845
+ prompt_to_use = self.session.prompt + "\n" + SUMMARY_REQUEST_PROMPT
846
+
847
+ summary = self.model.chat_until_success(prompt_to_use) # type: ignore
792
848
  # 防御: 可能返回空响应(None或空字符串),统一为空字符串并告警
793
849
  if not summary:
794
850
  try:
@@ -1003,7 +1059,6 @@ class Agent:
1003
1059
  {complete_prompt}
1004
1060
  如果没有完成,请进行下一步操作:
1005
1061
  - 仅包含一个操作
1006
- - 不要询问用户是否继续,直接继续执行直至完成
1007
1062
  - 如果信息不明确,请请求用户补充
1008
1063
  - 如果执行过程中连续失败5次,请使用ask_user询问用户操作
1009
1064
  - 操作列表:{action_handlers}{memory_prompts}
@@ -1043,6 +1098,13 @@ class Agent:
1043
1098
  )
1044
1099
  except Exception:
1045
1100
  pass
1101
+ # 如启用规划模式,先判断是否需要拆分并调度子任务
1102
+ if self.plan:
1103
+ try:
1104
+ self._maybe_plan_and_dispatch(self.session.prompt)
1105
+ except Exception:
1106
+ # 防御式处理,规划失败不影响主流程
1107
+ pass
1046
1108
  return self._main_loop()
1047
1109
  except Exception as e:
1048
1110
  PrettyOutput.print(f"任务失败: {str(e)}", OutputType.ERROR)
@@ -1150,6 +1212,55 @@ class Agent:
1150
1212
  temp_model.set_system_prompt(system_prompt)
1151
1213
  return temp_model
1152
1214
 
1215
+ def _build_child_agent_params(self, name: str, description: str) -> Dict[str, Any]:
1216
+ """构建子Agent参数,尽量继承父Agent配置,并确保子Agent非交互自动完成。"""
1217
+ use_tools_param: Optional[List[str]] = None
1218
+ try:
1219
+ tr = self.get_tool_registry()
1220
+ if isinstance(tr, ToolRegistry):
1221
+ selected_tools = tr.get_all_tools()
1222
+ use_tools_param = [t["name"] for t in selected_tools]
1223
+ except Exception:
1224
+ use_tools_param = None
1225
+
1226
+ return {
1227
+ "system_prompt": origin_agent_system_prompt,
1228
+ "name": name,
1229
+ "description": description,
1230
+ "model_group": self.model_group,
1231
+ "summary_prompt": self.summary_prompt,
1232
+ "auto_complete": True,
1233
+ "use_tools": use_tools_param,
1234
+ "execute_tool_confirm": self.execute_tool_confirm,
1235
+ "need_summary": self.need_summary,
1236
+ "auto_summary_rounds": self.auto_summary_rounds,
1237
+ "multiline_inputer": self.multiline_inputer,
1238
+ "use_methodology": self.use_methodology,
1239
+ "use_analysis": self.use_analysis,
1240
+ "force_save_memory": self.force_save_memory,
1241
+ "files": self.files,
1242
+ "confirm_callback": self.confirm_callback,
1243
+ "non_interactive": True,
1244
+ "in_multi_agent": True,
1245
+ "plan": self.plan, # 继承父Agent的规划开关
1246
+ "plan_depth": self.plan_depth + 1, # 子Agent层数+1
1247
+ "plan_max_depth": self.plan_max_depth, # 继承上限
1248
+ }
1249
+
1250
+ def _maybe_plan_and_dispatch(self, task_text: str) -> None:
1251
+ """委托给 TaskPlanner 执行任务规划与子任务调度,保持向后兼容。"""
1252
+ try:
1253
+ if hasattr(self, "task_planner") and self.task_planner:
1254
+ # 优先使用初始化时注入的规划器
1255
+ self.task_planner.maybe_plan_and_dispatch(task_text) # type: ignore[attr-defined]
1256
+ else:
1257
+ # 防御式回退:临时创建规划器以避免因未初始化导致的崩溃
1258
+ from jarvis.jarvis_agent.task_planner import TaskPlanner
1259
+ TaskPlanner(self, plan_depth=self.plan_depth, plan_max_depth=self.plan_max_depth).maybe_plan_and_dispatch(task_text)
1260
+ except Exception:
1261
+ # 规划失败不影响主流程
1262
+ pass
1263
+
1153
1264
  def _filter_tools_if_needed(self, task: str):
1154
1265
  """如果工具数量超过阈值,使用大模型筛选相关工具"""
1155
1266
  tool_registry = self.get_tool_registry()
@@ -681,6 +681,7 @@ def run_cli(
681
681
  non_interactive: bool = typer.Option(
682
682
  False, "-n", "--non-interactive", help="启用非交互模式:用户无法与命令交互,脚本执行超时限制为5分钟"
683
683
  ),
684
+ plan: bool = typer.Option(False, "--plan/--no-plan", help="启用或禁用任务规划(拆分子任务并汇总执行结果)"),
684
685
  web: bool = typer.Option(False, "--web", help="以 Web 模式启动,通过浏览器 WebSocket 交互"),
685
686
  web_host: str = typer.Option("127.0.0.1", "--web-host", help="Web 服务主机"),
686
687
  web_port: int = typer.Option(8765, "--web-port", help="Web 服务端口"),
@@ -1022,6 +1023,11 @@ def run_cli(
1022
1023
  **extra_kwargs,
1023
1024
  )
1024
1025
  agent = agent_manager.initialize()
1026
+ # CLI 开关:启用/禁用规划(不依赖 AgentManager 支持,直接设置 Agent 属性)
1027
+ try:
1028
+ agent.plan = bool(plan)
1029
+ except Exception:
1030
+ pass
1025
1031
 
1026
1032
  if web:
1027
1033
  try:
@@ -49,7 +49,14 @@ class ShareManager(ABC):
49
49
  def __init__(self, central_repo_url: str, repo_name: str):
50
50
  self.central_repo_url = central_repo_url
51
51
  self.repo_name = repo_name
52
- self.repo_path = os.path.join(get_data_dir(), repo_name)
52
+ # 支持将中心仓库配置为本地目录(含git子路径)
53
+ expanded = os.path.expanduser(os.path.expandvars(central_repo_url))
54
+ if os.path.isdir(expanded):
55
+ # 直接使用本地目录作为中心仓库路径(支持git仓库子目录)
56
+ self.repo_path = expanded
57
+ else:
58
+ # 仍按原逻辑使用数据目录中的克隆路径
59
+ self.repo_path = os.path.join(get_data_dir(), repo_name)
53
60
 
54
61
  def update_central_repo(self) -> None:
55
62
  """克隆或更新中心仓库"""
@@ -0,0 +1,218 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ TaskPlanner: 任务规划与子任务调度器
4
+
5
+ 职责:
6
+ - 判断是否需要拆分任务
7
+ - 解析 <PLAN> YAML 列表
8
+ - 为每个子任务创建子Agent并执行
9
+ - 汇总所有子任务执行结果并写回父Agent上下文(包含 <PLAN>/<SUB_TASK_RESULTS>/<RESULT_SUMMARY>)
10
+ """
11
+
12
+ from typing import Any, List
13
+ import re
14
+
15
+ import yaml # type: ignore
16
+
17
+ from jarvis.jarvis_agent.utils import join_prompts
18
+ from jarvis.jarvis_utils.output import OutputType, PrettyOutput
19
+
20
+
21
+ class TaskPlanner:
22
+ """将 Agent 的任务规划逻辑封装为独立类,便于维护与复用。"""
23
+
24
+ def __init__(self, agent: Any, plan_depth: int = 0, plan_max_depth: int = 2) -> None:
25
+ """
26
+ 参数:
27
+ agent: 父Agent实例(须提供以下能力)
28
+ - _create_temp_model(system_prompt: str) -> BasePlatform
29
+ - _build_child_agent_params(name: str, description: str) -> dict
30
+ - name, session, plan 等属性
31
+ plan_depth: 当前规划深度(由外部在构造时传入)
32
+ plan_max_depth: 规划最大深度(由外部在构造时传入)
33
+ """
34
+ self.agent = agent
35
+ try:
36
+ self.plan_depth = int(plan_depth)
37
+ except Exception:
38
+ self.plan_depth = 0
39
+ try:
40
+ self.plan_max_depth = int(plan_max_depth)
41
+ except Exception:
42
+ self.plan_max_depth = 2
43
+
44
+ def maybe_plan_and_dispatch(self, task_text: str) -> None:
45
+ """
46
+ 当启用 agent.plan 时,调用临时模型评估是否需要拆分任务并执行子任务。
47
+ - 若模型返回 <DONT_NEED/>,则直接返回不做任何修改;
48
+ - 若返回 <SUB_TASK> 块,则解析每行以“- ”开头的子任务,逐个创建子Agent执行;
49
+ - 将子任务与结果以结构化块写回到 agent.session.prompt,随后由主循环继续处理。
50
+ """
51
+ if not getattr(self.agent, "plan", False):
52
+ return
53
+
54
+ # 深度限制检查:当当前规划深度已达到或超过上限时,禁止继续规划
55
+ try:
56
+ current_depth = int(self.plan_depth)
57
+ except Exception:
58
+ current_depth = 0
59
+ try:
60
+ max_depth = int(self.plan_max_depth)
61
+ except Exception:
62
+ max_depth = 2
63
+
64
+ if current_depth >= max_depth:
65
+ PrettyOutput.print(
66
+ f"已达到任务规划最大深度({max_depth}),本层不再进行规划。", OutputType.INFO
67
+ )
68
+ return
69
+
70
+ try:
71
+ PrettyOutput.print("任务规划启动,评估是否需要拆分...", OutputType.INFO)
72
+ planning_sys = (
73
+ "你是一个任务规划助手。请判断是否需要拆分任务。\n"
74
+ "当需要拆分时,仅按以下结构输出:\n"
75
+ "<PLAN>\n- 子任务1\n- 子任务2\n</PLAN>\n"
76
+ "示例:\n"
77
+ "<PLAN>\n- 分析当前任务,提取需要修改的文件列表\n- 修改配置默认值并更新相关 schema\n- 更新文档中对该默认值的描述\n</PLAN>\n"
78
+ "要求:<PLAN> 内必须是有效 YAML 列表,仅包含字符串项;禁止输出任何额外解释。\n"
79
+ "当不需要拆分时,仅输出:\n<DONT_NEED/>\n"
80
+ "禁止输出任何额外解释。"
81
+ )
82
+ temp_model = self.agent._create_temp_model(planning_sys)
83
+ plan_prompt = f"任务:\n{task_text}\n\n请严格按要求只输出结构化标签块。"
84
+ plan_resp = temp_model.chat_until_success(plan_prompt) # type: ignore
85
+ if not plan_resp:
86
+ PrettyOutput.print("任务规划模型未返回有效响应。", OutputType.WARNING)
87
+ return
88
+ except Exception as e:
89
+ # 规划失败不影响主流程
90
+ PrettyOutput.print(f"任务规划失败: {e}", OutputType.ERROR)
91
+ return
92
+
93
+ text = str(plan_resp).strip()
94
+ # 不需要拆分
95
+ if re.search(r"<\s*DONT_NEED\s*/\s*>", text, re.IGNORECASE):
96
+ PrettyOutput.print("任务规划完成:无需拆分。", OutputType.SUCCESS)
97
+ return
98
+
99
+ # 解析 <SUB_TASK> 块
100
+ m = re.search(
101
+ r"<\s*PLAN\s*>\s*(.*?)\s*<\s*/\s*PLAN\s*>",
102
+ text,
103
+ re.IGNORECASE | re.DOTALL,
104
+ )
105
+ subtasks: List[str] = []
106
+ if m:
107
+ block = m.group(1)
108
+ try:
109
+ data = yaml.safe_load(block)
110
+ if isinstance(data, list):
111
+ for item in data:
112
+ if isinstance(item, str):
113
+ s = item.strip()
114
+ if s:
115
+ subtasks.append(s)
116
+ else:
117
+ PrettyOutput.print("任务规划提示:无需拆分。", OutputType.INFO)
118
+ except Exception as e:
119
+ PrettyOutput.print("任务规划提示:无需拆分。", OutputType.INFO)
120
+ else:
121
+ PrettyOutput.print("任务规划提示:无需拆分。", OutputType.INFO)
122
+
123
+ if not subtasks:
124
+ # 无有效子任务,直接返回
125
+ PrettyOutput.print("任务规划提示:无需拆分。", OutputType.INFO)
126
+ return
127
+
128
+ PrettyOutput.print(f"任务已拆分为 {len(subtasks)} 个子任务:", OutputType.SUCCESS)
129
+ for i, st in enumerate(subtasks, 1):
130
+ PrettyOutput.print(f" {i}. {st}", OutputType.INFO)
131
+
132
+ # 执行子任务
133
+ executed_subtask_block_lines: List[str] = ["<PLAN>"]
134
+ executed_subtask_block_lines += [f"- {t}" for t in subtasks]
135
+ executed_subtask_block_lines.append("</PLAN>")
136
+
137
+ results_lines: List[str] = []
138
+ for i, st in enumerate(subtasks, 1):
139
+ try:
140
+ PrettyOutput.print(f"开始执行子任务 {i}/{len(subtasks)}: {st}", OutputType.INFO)
141
+ child_kwargs = self.agent._build_child_agent_params(
142
+ name=f"{self.agent.name}-child-{i}",
143
+ description=f"子任务执行器: {st}",
144
+ )
145
+ # 使用父Agent的类创建子Agent,避免循环依赖
146
+ child = self.agent.__class__(**child_kwargs)
147
+ # 构造子任务执行提示,包含父任务与前置子任务结果,避免背景缺失
148
+ subtask_block_text = "\n".join(executed_subtask_block_lines)
149
+ if results_lines:
150
+ prev_results_block = "<PREVIOUS_SUB_TASK_RESULTS>\n" + "\n".join(results_lines) + "\n</PREVIOUS_SUB_TASK_RESULTS>"
151
+ else:
152
+ prev_results_block = "<PREVIOUS_SUB_TASK_RESULTS />"
153
+ child_prompt = join_prompts([
154
+ f"原始任务:\n{task_text}",
155
+ f"子任务规划:\n{subtask_block_text}",
156
+ f"前置子任务执行结果:\n{prev_results_block}",
157
+ f"当前子任务:{st}",
158
+ "请基于原始任务背景与前置结果执行当前子任务,避免重复工作;如需依赖前置产物请直接复用;如需为后续子任务提供数据,请妥善保存(可使用工具保存文件或记忆)。"
159
+ ])
160
+ child_result = child.run(child_prompt)
161
+ result_text = "" if child_result is None else str(child_result)
162
+ # 防止极端长输出导致污染,这里不做截断,交由上层摘要策略控制
163
+ results_lines.append(f"- 子任务{i}: {st}\n 结果: {result_text}")
164
+ PrettyOutput.print(f"子任务 {i}/{len(subtasks)} 执行完成。", OutputType.SUCCESS)
165
+ except Exception as e:
166
+ results_lines.append(f"- 子任务{i}: {st}\n 结果: 执行失败,原因: {e}")
167
+ PrettyOutput.print(f"子任务 {i}/{len(subtasks)} 执行失败: {e}", OutputType.ERROR)
168
+
169
+ subtask_block = "\n".join(executed_subtask_block_lines)
170
+ results_block = "<SUB_TASK_RESULTS>\n" + "\n".join(results_lines) + "\n</SUB_TASK_RESULTS>"
171
+
172
+ PrettyOutput.print("所有子任务执行完毕,正在整合结果...", OutputType.INFO)
173
+ # 先对所有子任务结果进行简要自动汇总,便于父Agent继续整合
174
+ summary_block = "<RESULT_SUMMARY>\n无摘要(将直接使用结果详情继续)\n</RESULT_SUMMARY>"
175
+ try:
176
+ summarizing_sys = (
177
+ "你是一个任务结果整合助手。请根据提供的原始任务、子任务清单与子任务执行结果,"
178
+ "生成简明扼要的汇总与关键结论,突出已完成项、遗留风险与下一步建议。"
179
+ "严格仅输出以下结构:\n"
180
+ "<RESULT_SUMMARY>\n"
181
+ "…你的简要汇总…\n"
182
+ "</RESULT_SUMMARY>\n"
183
+ "不要输出其他任何解释。"
184
+ )
185
+ temp_model2 = self.agent._create_temp_model(summarizing_sys)
186
+ sum_prompt = (
187
+ f"原始任务:\n{task_text}\n\n"
188
+ f"子任务规划:\n{subtask_block}\n\n"
189
+ f"子任务执行结果:\n{results_block}\n\n"
190
+ "请按要求仅输出汇总块。"
191
+ )
192
+ sum_resp = temp_model2.chat_until_success(sum_prompt) # type: ignore
193
+ if isinstance(sum_resp, str) and sum_resp.strip():
194
+ s = sum_resp.strip()
195
+ if not re.search(r"<\s*RESULT_SUMMARY\s*>", s, re.IGNORECASE):
196
+ s = f"<RESULT_SUMMARY>\n{s}\n</RESULT_SUMMARY>"
197
+ summary_block = s
198
+ except Exception:
199
+ # 汇总失败不影响主流程,继续使用默认占位
200
+ pass
201
+
202
+ # 合并回父Agent的 prompt,父Agent将基于汇总与详情继续执行
203
+ try:
204
+ self.agent.session.prompt = join_prompts(
205
+ [
206
+ f"原始任务:\n{task_text}",
207
+ f"子任务规划:\n{subtask_block}",
208
+ f"子任务结果汇总:\n{summary_block}",
209
+ f"子任务执行结果:\n{results_block}",
210
+ "请基于上述子任务结果整合并完成最终输出。",
211
+ ]
212
+ )
213
+ except Exception:
214
+ # 回退拼接
215
+ self.agent.session.prompt = (
216
+ f"{task_text}\n\n{subtask_block}\n\n{summary_block}\n\n{results_block}\n\n"
217
+ "请基于上述子任务结果整合并完成最终输出。"
218
+ )
@@ -25,6 +25,7 @@ from jarvis.jarvis_utils.config import (
25
25
  is_enable_static_analysis,
26
26
  get_git_check_mode,
27
27
  set_config,
28
+ get_data_dir,
28
29
  )
29
30
  from jarvis.jarvis_utils.git_utils import (
30
31
  confirm_add_new_files,
@@ -39,7 +40,7 @@ from jarvis.jarvis_utils.git_utils import (
39
40
  )
40
41
  from jarvis.jarvis_utils.input import get_multiline_input, user_confirm
41
42
  from jarvis.jarvis_utils.output import OutputType, PrettyOutput
42
- from jarvis.jarvis_utils.utils import get_loc_stats, init_env
43
+ from jarvis.jarvis_utils.utils import get_loc_stats, init_env, _acquire_single_instance_lock
43
44
 
44
45
  app = typer.Typer(help="Jarvis 代码助手")
45
46
 
@@ -57,6 +58,8 @@ class CodeAgent:
57
58
  append_tools: Optional[str] = None,
58
59
  tool_group: Optional[str] = None,
59
60
  non_interactive: Optional[bool] = None,
61
+ plan: Optional[bool] = None,
62
+ **kwargs,
60
63
  ):
61
64
  self.root_dir = os.getcwd()
62
65
  self.tool_group = tool_group
@@ -86,6 +89,22 @@ class CodeAgent:
86
89
 
87
90
  tool_registry.use_tools(base_tools)
88
91
  code_system_prompt = self._get_system_prompt()
92
+ # 先加载全局规则(数据目录 rules),再加载项目规则(.jarvis/rules),并拼接为单一规则块注入
93
+ global_rules = self._read_global_rules()
94
+ project_rules = self._read_project_rules()
95
+
96
+ combined_parts: List[str] = []
97
+ if global_rules:
98
+ combined_parts.append(global_rules)
99
+ if project_rules:
100
+ combined_parts.append(project_rules)
101
+
102
+ if combined_parts:
103
+ merged_rules = "\n\n".join(combined_parts)
104
+ code_system_prompt = (
105
+ f"{code_system_prompt}\n\n"
106
+ f"<rules>\n{merged_rules}\n</rules>"
107
+ )
89
108
  self.agent = Agent(
90
109
  system_prompt=code_system_prompt,
91
110
  name="CodeAgent",
@@ -95,6 +114,7 @@ class CodeAgent:
95
114
  use_methodology=False, # 禁用方法论
96
115
  use_analysis=False, # 禁用分析
97
116
  non_interactive=self.non_interactive,
117
+ plan=bool(plan) if plan is not None else False,
98
118
  )
99
119
 
100
120
  self.agent.event_bus.subscribe(AFTER_TOOL_CALL, self._on_after_tool_call)
@@ -164,6 +184,32 @@ class CodeAgent:
164
184
  </say_to_llm>
165
185
  """
166
186
 
187
+ def _read_project_rules(self) -> Optional[str]:
188
+ """读取 .jarvis/rules 内容,如果存在则返回字符串,否则返回 None"""
189
+ try:
190
+ rules_path = os.path.join(self.root_dir, ".jarvis", "rules")
191
+ if os.path.exists(rules_path) and os.path.isfile(rules_path):
192
+ with open(rules_path, "r", encoding="utf-8", errors="replace") as f:
193
+ content = f.read().strip()
194
+ return content if content else None
195
+ except Exception:
196
+ # 读取规则失败时忽略,不影响主流程
197
+ pass
198
+ return None
199
+
200
+ def _read_global_rules(self) -> Optional[str]:
201
+ """读取数据目录 rules 内容,如果存在则返回字符串,否则返回 None"""
202
+ try:
203
+ rules_path = os.path.join(get_data_dir(), "rules")
204
+ if os.path.exists(rules_path) and os.path.isfile(rules_path):
205
+ with open(rules_path, "r", encoding="utf-8", errors="replace") as f:
206
+ content = f.read().strip()
207
+ return content if content else None
208
+ except Exception:
209
+ # 读取规则失败时忽略,不影响主流程
210
+ pass
211
+ return None
212
+
167
213
  def _check_git_config(self) -> None:
168
214
  """检查 git username 和 email 是否已设置,如果没有则提示并退出"""
169
215
  try:
@@ -672,12 +718,58 @@ class CodeAgent:
672
718
  def _build_per_file_patch_preview(modified_files: List[str]) -> str:
673
719
  status_map = _build_name_status_map()
674
720
  lines: List[str] = []
721
+
722
+ def _get_file_numstat(file_path: str) -> Tuple[int, int]:
723
+ """获取单文件的新增/删除行数,失败时返回(0,0)"""
724
+ head_exists = bool(get_latest_commit_hash())
725
+ try:
726
+ # 让未跟踪文件也能统计到新增行数
727
+ subprocess.run(["git", "add", "-N", "--", file_path], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
728
+ cmd = ["git", "diff", "--numstat"] + (["HEAD"] if head_exists else []) + ["--", file_path]
729
+ res = subprocess.run(
730
+ cmd,
731
+ capture_output=True,
732
+ text=True,
733
+ encoding="utf-8",
734
+ errors="replace",
735
+ check=False,
736
+ )
737
+ if res.returncode == 0 and res.stdout:
738
+ for line in res.stdout.splitlines():
739
+ parts = line.strip().split("\t")
740
+ if len(parts) >= 3:
741
+ add_s, del_s = parts[0], parts[1]
742
+
743
+ def to_int(x: str) -> int:
744
+ try:
745
+ return int(x)
746
+ except Exception:
747
+ # 二进制或无法解析时显示为0
748
+ return 0
749
+
750
+ return to_int(add_s), to_int(del_s)
751
+ finally:
752
+ subprocess.run(["git", "reset", "--", file_path], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
753
+ return (0, 0)
754
+
675
755
  for f in modified_files:
676
756
  status = status_map.get(f, "")
677
- # 删除文件:不展示diff,仅提示
757
+ adds, dels = _get_file_numstat(f)
758
+ total_changes = adds + dels
759
+
760
+ # 删除文件:不展示diff,仅提示(附带删除行数信息如果可用)
678
761
  if (status.startswith("D")) or (not os.path.exists(f)):
679
- lines.append(f"- {f} 文件被删除")
762
+ if dels > 0:
763
+ lines.append(f"- {f} 文件被删除(删除{dels}行)")
764
+ else:
765
+ lines.append(f"- {f} 文件被删除")
680
766
  continue
767
+
768
+ # 变更过大:仅提示新增/删除行数,避免输出超长diff
769
+ if total_changes > 300:
770
+ lines.append(f"- {f} 新增{adds}行/删除{dels}行(变更过大,预览已省略)")
771
+ continue
772
+
681
773
  # 其它情况:展示该文件的diff
682
774
  file_diff = _get_file_diff(f)
683
775
  if file_diff.strip():
@@ -805,6 +897,7 @@ def cli(
805
897
  non_interactive: bool = typer.Option(
806
898
  False, "-n", "--non-interactive", help="启用非交互模式:用户无法与命令交互,脚本执行超时限制为5分钟"
807
899
  ),
900
+ plan: bool = typer.Option(False, "--plan/--no-plan", help="启用或禁用任务规划(子任务拆分与汇总执行)"),
808
901
  ) -> None:
809
902
  """Jarvis主入口点。"""
810
903
  # CLI 标志:非交互模式(不依赖配置文件)
@@ -825,6 +918,8 @@ def cli(
825
918
  "欢迎使用 Jarvis-CodeAgent,您的代码工程助手已准备就绪!",
826
919
  config_file=config_file,
827
920
  )
921
+ # CodeAgent 单实例互斥:仅代码助手入口加锁,其他入口不受影响
922
+ _acquire_single_instance_lock(lock_name="code_agent.lock")
828
923
 
829
924
  # 在初始化环境后同步 CLI 选项到全局配置,避免被 init_env 覆盖
830
925
  try:
@@ -884,6 +979,7 @@ def cli(
884
979
  append_tools=append_tools,
885
980
  tool_group=tool_group,
886
981
  non_interactive=non_interactive,
982
+ plan=plan,
887
983
  )
888
984
 
889
985
  # 尝试恢复会话