jarvis-ai-assistant 0.5.0__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.
jarvis/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """Jarvis AI Assistant"""
3
3
 
4
- __version__ = "0.5.0"
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 (
@@ -285,6 +285,21 @@ class Agent:
285
285
  """获取工具使用提示"""
286
286
  return build_action_prompt(self.output_handler) # type: ignore
287
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
+
288
303
  def __init__(
289
304
  self,
290
305
  system_prompt: str,
@@ -306,6 +321,10 @@ class Agent:
306
321
  confirm_callback: Optional[Callable[[str, bool], bool]] = None,
307
322
  non_interactive: Optional[bool] = None,
308
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",
309
328
  **kwargs,
310
329
  ):
311
330
  """初始化Jarvis Agent实例
@@ -325,6 +344,9 @@ class Agent:
325
344
  force_save_memory: 是否强制保存记忆
326
345
  confirm_callback: 用户确认回调函数,签名为 (tip: str, default: bool) -> bool;默认使用CLI的user_confirm
327
346
  non_interactive: 是否以非交互模式运行(优先级最高,覆盖环境变量与配置)
347
+ plan: 是否启用任务规划与子任务拆分(默认 False;启用后在进入主循环前评估是否需要将任务拆分为 <SUB_TASK> 列表,逐一由子Agent执行并汇总结果)
348
+ plan_max_depth: 任务规划的最大层数(默认3,可通过配置 JARVIS_PLAN_MAX_DEPTH 或入参覆盖)
349
+ plan_depth: 当前规划层数(内部用于递归控制,子Agent会在父基础上+1)
328
350
  """
329
351
  # 基础属性初始化(仅根据入参设置原始值;实际生效的默认回退在 _init_config 中统一解析)
330
352
  # 标识与描述
@@ -348,6 +370,18 @@ class Agent:
348
370
  self.non_interactive = non_interactive
349
371
  # 多智能体运行标志:用于控制非交互模式下的自动完成行为
350
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
351
385
  # 运行时状态
352
386
  self.first = True
353
387
  self.run_input_handlers_next_turn = False
@@ -429,6 +463,8 @@ class Agent:
429
463
  self.task_analyzer = TaskAnalyzer(self)
430
464
  self.file_methodology_manager = FileMethodologyManager(self)
431
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)
432
468
 
433
469
  # 设置系统提示词
434
470
  self._setup_system_prompt()
@@ -801,9 +837,14 @@ class Agent:
801
837
  try:
802
838
  if not self.model:
803
839
  raise RuntimeError("Model not initialized")
804
- summary = self.model.chat_until_success(
805
- self.session.prompt + "\n" + SUMMARY_REQUEST_PROMPT
806
- ) # 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
807
848
  # 防御: 可能返回空响应(None或空字符串),统一为空字符串并告警
808
849
  if not summary:
809
850
  try:
@@ -1057,6 +1098,13 @@ class Agent:
1057
1098
  )
1058
1099
  except Exception:
1059
1100
  pass
1101
+ # 如启用规划模式,先判断是否需要拆分并调度子任务
1102
+ if self.plan:
1103
+ try:
1104
+ self._maybe_plan_and_dispatch(self.session.prompt)
1105
+ except Exception:
1106
+ # 防御式处理,规划失败不影响主流程
1107
+ pass
1060
1108
  return self._main_loop()
1061
1109
  except Exception as e:
1062
1110
  PrettyOutput.print(f"任务失败: {str(e)}", OutputType.ERROR)
@@ -1164,6 +1212,55 @@ class Agent:
1164
1212
  temp_model.set_system_prompt(system_prompt)
1165
1213
  return temp_model
1166
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
+
1167
1264
  def _filter_tools_if_needed(self, task: str):
1168
1265
  """如果工具数量超过阈值,使用大模型筛选相关工具"""
1169
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:
@@ -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
+ )
@@ -40,7 +40,7 @@ from jarvis.jarvis_utils.git_utils import (
40
40
  )
41
41
  from jarvis.jarvis_utils.input import get_multiline_input, user_confirm
42
42
  from jarvis.jarvis_utils.output import OutputType, PrettyOutput
43
- 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
44
44
 
45
45
  app = typer.Typer(help="Jarvis 代码助手")
46
46
 
@@ -58,6 +58,8 @@ class CodeAgent:
58
58
  append_tools: Optional[str] = None,
59
59
  tool_group: Optional[str] = None,
60
60
  non_interactive: Optional[bool] = None,
61
+ plan: Optional[bool] = None,
62
+ **kwargs,
61
63
  ):
62
64
  self.root_dir = os.getcwd()
63
65
  self.tool_group = tool_group
@@ -112,6 +114,7 @@ class CodeAgent:
112
114
  use_methodology=False, # 禁用方法论
113
115
  use_analysis=False, # 禁用分析
114
116
  non_interactive=self.non_interactive,
117
+ plan=bool(plan) if plan is not None else False,
115
118
  )
116
119
 
117
120
  self.agent.event_bus.subscribe(AFTER_TOOL_CALL, self._on_after_tool_call)
@@ -894,6 +897,7 @@ def cli(
894
897
  non_interactive: bool = typer.Option(
895
898
  False, "-n", "--non-interactive", help="启用非交互模式:用户无法与命令交互,脚本执行超时限制为5分钟"
896
899
  ),
900
+ plan: bool = typer.Option(False, "--plan/--no-plan", help="启用或禁用任务规划(子任务拆分与汇总执行)"),
897
901
  ) -> None:
898
902
  """Jarvis主入口点。"""
899
903
  # CLI 标志:非交互模式(不依赖配置文件)
@@ -914,6 +918,8 @@ def cli(
914
918
  "欢迎使用 Jarvis-CodeAgent,您的代码工程助手已准备就绪!",
915
919
  config_file=config_file,
916
920
  )
921
+ # CodeAgent 单实例互斥:仅代码助手入口加锁,其他入口不受影响
922
+ _acquire_single_instance_lock(lock_name="code_agent.lock")
917
923
 
918
924
  # 在初始化环境后同步 CLI 选项到全局配置,避免被 init_env 覆盖
919
925
  try:
@@ -973,6 +979,7 @@ def cli(
973
979
  append_tools=append_tools,
974
980
  tool_group=tool_group,
975
981
  non_interactive=non_interactive,
982
+ plan=plan,
976
983
  )
977
984
 
978
985
  # 尝试恢复会话
@@ -188,6 +188,11 @@
188
188
  "description": "AI工具筛选阈值:当可用工具数量超过此值时触发AI筛选",
189
189
  "default": 30
190
190
  },
191
+ "JARVIS_PLAN_MAX_DEPTH": {
192
+ "type": "number",
193
+ "description": "任务规划的最大层数。用于限制 plan 模式的递归拆分深度。仅在启用规划时生效(通过 CLI --plan/--no-plan 控制),默认 3。",
194
+ "default": 2
195
+ },
191
196
  "JARVIS_SCRIPT_EXECUTION_TIMEOUT": {
192
197
  "type": "number",
193
198
  "description": "脚本执行的超时时间(秒),仅在非交互模式下生效。",
@@ -196,7 +201,7 @@
196
201
  "JARVIS_AUTO_SUMMARY_ROUNDS": {
197
202
  "type": "number",
198
203
  "description": "基于对话轮次的自动总结阈值(达到该轮次后自动总结并清理历史)",
199
- "default": 20
204
+ "default": 50
200
205
  },
201
206
  "JARVIS_CONFIRM_BEFORE_APPLY_PATCH": {
202
207
  "type": "boolean",
@@ -0,0 +1,180 @@
1
+ OpenHarmony 安全演进套件(jarvis_sec)
2
+ ================================
3
+
4
+ 概览
5
+ - 模式:单 Agent 逐条子任务分析(先直扫拆分候选,再由 Agent 精核)
6
+ - 目标:在不破坏现有功能的前提下,增强稳定性与可追溯性,减少“全部解析失败”的概率
7
+ - 关键特性:
8
+ - 禁用方法论与分析(use_methodology=False, use_analysis=False),降低不确定性
9
+ - 按次指定模型组(--llm-group / llm_group 参数),不修改全局配置
10
+ - 启用摘要 need_summary=True,通过 <REPORT>…</REPORT> 输出结构化结果
11
+ - 进度与过程日志输出(Progress / parse-fail / no-issue / issues-found)
12
+ - 增量写入 JSONL 报告,长任务中可实时查看
13
+ - 只读约束:禁止任何写文件或破坏性命令(rm/mv/cp/echo >/sed -i/git/patch/chmod/chown 等)
14
+
15
+ CLI 使用
16
+ - 入口:python -m jarvis.jarvis_sec.cli agent --path ./target_project
17
+ - 可选参数:
18
+ - --languages/-l:逗号分隔的扩展名列表,例如 "c,cpp,h,hpp,rs"
19
+ - --llm-group/-g:本次运行使用的模型组(仅透传,不改全局)
20
+ - --report-file/-r:JSONL 报告输出路径(默认写入 path/.jarvis/sec/agent_issues.jsonl)
21
+
22
+ 示例
23
+ - 最简运行:
24
+ python -m jarvis.jarvis_sec.cli agent -p ./path/to/project
25
+
26
+ - 指定语言与临时模型组:
27
+ python -m jarvis.jarvis_sec.cli agent -p ./proj -l c,cpp,h,hpp,rs -g my_llm_group
28
+
29
+ - 指定增量报告文件:
30
+ python -m jarvis.jarvis_sec.cli agent -p ./proj -r ./out/agent_issues.jsonl
31
+
32
+ 工作流要点
33
+ 1) 直扫(direct_scan)
34
+ - 使用正则/命令行辅助在本地生成候选问题(issues)与统计信息(summary)
35
+ - 可在无外部服务时复现与回退,保障可用性
36
+
37
+ 2) 子任务拆分与单 Agent 精核
38
+ - 将每个候选压缩为精简上下文(language、category、pattern、file、line、evidence 等)
39
+ - 单 Agent 周期内:
40
+ - need_summary=True
41
+ - summary_prompt 为 _build_summary_prompt(...) 返回的提示,要求在 <REPORT>…</REPORT> 内输出 JSON 或 YAML
42
+ - 系统提示包含只读约束与“一次仅执行一个工具”规则
43
+ - 推荐工具:read_code(读取目标文件附近源码)、execute_script(只读检索,如 rg/find)
44
+
45
+ 3) 解析策略
46
+ - 优先解析摘要(agent.generate_summary())中的 <REPORT>…</REPORT>:
47
+ - _try_parse_summary_report 支持 JSON 优先,失败回退 YAML(safe_load)
48
+ - 摘要不可解析时,直接判定 parse-fail(不会回退解析主输出)
49
+
50
+ 4) 增量写入 JSONL
51
+ - 每个子任务如检测到 issues,立即将记录 append 到 JSONL(默认 path/.jarvis/sec/agent_issues.jsonl)
52
+ - 支持通过 --report-file/-r 指定其他路径
53
+ - 失败不会影响主流程
54
+
55
+ JSONL 记录结构
56
+ - 每行一个 JSON 对象,格式如下:
57
+ {
58
+ "task_id": "JARVIS-SEC-Analyzer-3",
59
+ "candidate": {
60
+ "language": "c/cpp",
61
+ "category": "unsafe_api",
62
+ "pattern": "strcpy",
63
+ "file": "src/foo.c",
64
+ "line": 120,
65
+ "evidence": "strcpy(dst, src);",
66
+ "confidence": 0.9,
67
+ "severity": "high"
68
+ },
69
+ "issues": [
70
+ {
71
+ "language": "c/cpp",
72
+ "category": "unsafe_api",
73
+ "pattern": "strcpy",
74
+ "file": "src/foo.c",
75
+ "line": 120,
76
+ "evidence": "strcpy(dst, src);",
77
+ "description": "使用不安全/高风险字符串API,可能导致缓冲区溢出或格式化风险。",
78
+ "suggestion": "替换为带边界的安全API(如 snprintf/strlcpy 等)或加入显式长度检查。",
79
+ "confidence": 0.9,
80
+ "severity": "high"
81
+ }
82
+ ],
83
+ "meta": {
84
+ "entry_path": "/abs/path/to/project",
85
+ "languages": ["c","cpp","h","hpp","rs"],
86
+ "source": "summary"
87
+ "timestamp": "2025-10-19T03:00:00Z"
88
+ }
89
+ }
90
+
91
+ 摘要 <REPORT> 结构
92
+ - Agent 在摘要中必须只输出以下内容(推荐 JSON,支持 YAML):
93
+ <REPORT>
94
+ {
95
+ "issues": [
96
+ {
97
+ "language": "c/cpp|rust",
98
+ "category": "unsafe_api|buffer_overflow|memory_mgmt|error_handling|unsafe_usage|concurrency|ffi",
99
+ "pattern": "命中的模式/关键字",
100
+ "file": "相对或绝对路径",
101
+ "line": 0,
102
+ "evidence": "证据代码片段(单行简化)",
103
+ "description": "问题说明",
104
+ "suggestion": "修复建议",
105
+ "confidence": 0.0,
106
+ "severity": "high|medium|low"
107
+ }
108
+ ],
109
+ "meta": {
110
+ "task_id": "JARVIS-SEC-Analyzer-1",
111
+ "entry_path": "/abs/path",
112
+ "languages": ["c","cpp","h","hpp","rs"],
113
+ "candidate": { "...": "子任务精简信息" }
114
+ }
115
+ }
116
+ </REPORT>
117
+
118
+ - 要求:
119
+ - 仅在 <REPORT> 与 </REPORT> 中输出报告,不得出现其他文本
120
+ - 若确认误报,返回空数组 issues: []
121
+ - 字段值需与实际分析一致
122
+
123
+ 只读约束(强制)
124
+ - Agent 被要求仅做只读分析:禁止修改任何文件或执行写操作命令
125
+ - 禁止命令包含但不限于:rm/mv/cp/echo >、sed -i、git、patch、chmod、chown 等
126
+ - 推荐工具:
127
+ - read_code:按文件路径与行号范围读取源码(建议围绕候选行上下 50 行)
128
+ - execute_script:只读检索(如 rg/find/grep),避免任何写操作
129
+
130
+ 日志与可观测性
131
+ - 进度日志:
132
+ - [JARVIS-SEC] Progress i/N: file:line (lang)
133
+ - [JARVIS-SEC] no-issue i/N: ...
134
+ - [JARVIS-SEC] issues-found i/N: count=k -> append report (summary)
135
+ - [JARVIS-SEC] parse-fail i/N: ...
136
+ - JSONL 写入:
137
+ - [JARVIS-SEC] write K issue(s) to <path>
138
+
139
+ 模型组(llm_group)
140
+ - 通过 CLI 的 --llm-group 或 API 的 llm_group 参数传入,仅对本次调用链生效
141
+ - 不会覆盖全局配置(不调用 set_config)
142
+
143
+ 常见问题排查
144
+ - 解析失败(parse-fail):
145
+ - 确认模型已在摘要中输出 <REPORT>…</REPORT>
146
+ - 优先使用 JSON;YAML 解析依赖 PyYAML(safe_load),若环境无此库将忽略 YAML 回退
147
+ - 注意:不会回退解析主输出;若摘要缺失或格式不合规,将直接跳过该候选
148
+ - JSONL 未写入:
149
+ - 仅当 issues 非空时追加写入
150
+ - 确认 --report-file 或默认目录 path/.jarvis/sec/ 可写
151
+ - Agent 输出为空:
152
+ - CLI 会回退到直扫基线(run_security_analysis_fast),仍可得到 JSON+Markdown 报告
153
+
154
+ API 概览
155
+ - run_with_multi_agent(entry_path, languages=None, llm_group=None, report_file=None) -> str
156
+ - 透传到 run_security_analysis(...),实现“直扫 + 单 Agent 逐条验证 + 聚合”
157
+ - run_security_analysis_fast(entry_path, languages=None, exclude_dirs=None) -> str
158
+ - 纯直扫基线,返回 JSON + Markdown
159
+ - direct_scan(entry_path, languages=None, exclude_dirs=None) -> Dict
160
+ - 返回结构化 issues 与 summary
161
+ - run_with_multi_agent(entry_path, languages=None, llm_group=None, report_file=None) -> str
162
+ - 透传到 run_security_analysis(...),实现“直扫 + 单 Agent 逐条验证 + 聚合”
163
+ - run_security_analysis_fast(entry_path, languages=None, exclude_dirs=None) -> str
164
+ - 纯直扫基线,返回 JSON + Markdown
165
+ - direct_scan(entry_path, languages=None, exclude_dirs=None) -> Dict
166
+ - 返回结构化 issues 与 summary
167
+
168
+ 建议测试(可选)
169
+ - 摘要解析:
170
+ - _try_parse_summary_report 对 JSON/YAML/无 REPORT 的输入解析正确
171
+ - CLI 参数链路:
172
+ - --llm-group 仅透传,不改全局
173
+ - --report-file 写入指定路径
174
+ - 只读约束:
175
+ - 模拟 Agent 工具调用,确保拒绝写操作命令(可在提示词层面校验)
176
+
177
+ 版本兼容与注意事项
178
+ - 本模块不修改全局模型组配置
179
+ - 摘要使用 JSON 优先,YAML 为回退路径(需 PyYAML)
180
+ - 直扫基线可在无外部服务时独立运行,便于复现与回退