myagent-ai 1.10.6 → 1.10.7

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.
@@ -48,8 +48,9 @@ class MainAgent(BaseAgent):
48
48
  <toolstocal>
49
49
  <tool><beforecalltext>连接词,介绍调用什么工具,达到什么目的。</beforecalltext><toolname>工具名</toolname><parms>JSON格式的参数对象,例如: {"query": "搜索关键词", "num": 5}</parms><timeout>预估超时时限(秒)</timeout><callback>true/false,要求解析器在该工具执行完后是否要回调llm大模型,将所有工具输出结果+新构造的"context"输入给llm</callback></tool>
50
50
  </toolstocal>
51
- <remember>仅从最新用户输入(userprint 或 usersays_correct)中提炼值得长期记忆的信息(如用户偏好、重要结论、错误经验等)。不要从历史对话中重复提炼旧记忆。如果本轮用户输入没有新信息需要记忆,则为空。</remember>
51
+ <remember><type>global或session</type><content>仅从最新用户输入(userprint 或 usersays_correct)中提炼值得记忆的信息(如用户偏好、重要结论、错误经验等)。type=global表示跨会话全局记忆,type=session表示仅当前会话可用的记忆。如果本轮没有新信息需要记忆,则<content>为空、<type>不填。</content></remember>
52
52
  <recall>下一轮执行需要调取的记忆,这里要设计接上记忆库</recall>
53
+ <knowledge>从本轮对话或工具执行结果中提炼值得长期保存到知识库的专业知识、事实、经验法则、技术要点等。这些知识将被持久化存储,未来可通过 <get_knowledge> 检索复用。如果本轮没有需要保存的知识,则为空。格式要求:简洁明确,每条知识一行,用换行分隔。</knowledge>
53
54
  <get_knowledge>下一轮执行时需要从知识库搜索获得的知识,填写检索关键词或描述。如context中已包含充足的<knowledge>内容,则为空。如需更多专业知识支撑,则填写相关搜索词。</get_knowledge>
54
55
  <askuser>需要询问用户的内容,如无,则为空</askuser>
55
56
  <finish>true/false,是否结束循环调用llm。如"askuser"为非空,则"finish"输出true。否则,根据"context"判断任务是否已完成,是否结束llm回调</finish>
@@ -67,14 +68,15 @@ class MainAgent(BaseAgent):
67
68
  6. <parms>: **必须使用严格合法的JSON格式**,例如 {"query": "关键词", "num": 10},不要使用其他格式
68
69
  7. <timeout>: 预估超时秒数(简单操作10-30s,文件操作30-60s,网络请求60-120s,数据处理120-300s)
69
70
  8. <callback>: 如果该工具的执行结果对后续决策有影响,设为 true;否则设为 false
70
- 9. <remember>: 仅从最新用户输入(userprint usersays_correct)中提炼值得长期记忆的关键信息,不要重复提炼历史对话中已有的记忆。如果本轮没有新信息需要记忆,则为空
71
+ 9. <remember>: 包含 <type> 和 <content> 子标签。type 填 "global"(跨会话全局记忆)或 "session"(仅当前会话)。content 填从最新用户输入中提炼的值得记忆的关键信息。如果本轮无需记忆,content 为空且不填 type。注意:用户个人偏好、重要结论、通用经验用 global;当前任务的临时上下文、过程信息用 session
71
72
  10. <recall>: 描述下一轮执行时需要从记忆库中检索的内容关键词
72
- 11. <get_knowledge>: 如果当前 <knowledge> 内容不足以完成任务,填写需要从知识库搜索的关键词;否则为空
73
- 12. <askuser>: 当信息不足需要用户补充时,在此填写要问的问题
74
- 13. <finish>: 当任务已完成或需要等待用户回应时为 true;否则为 false 继续执行
75
- 14. <finish_reason>: **finish=true 时必须填写**,详细说明结束原因(任务完成/等待用户/信息不足/无法处理等)
76
- 15. <next_step>: **finish=false 时必须填写**,描述下一步计划做什么,要求简洁明确(1-2句话)
77
- 16. 使用中文输出所有内容
73
+ 11. <knowledge>: 从本轮对话或工具执行结果中提炼值得长期保存的专业知识、事实、经验法则、技术要点等。这些知识会被持久化到知识库文件,未来可通过 get_knowledge 检索复用。如果没有需要保存的知识,则为空。格式:简洁明确,每条知识一行
74
+ 12. <get_knowledge>: 如果当前 <knowledge> 内容不足以完成任务,填写需要从知识库搜索的关键词;否则为空
75
+ 13. <askuser>: 当信息不足需要用户补充时,在此填写要问的问题
76
+ 14. <finish>: 当任务已完成或需要等待用户回应时为 true;否则为 false 继续执行
77
+ 15. <finish_reason>: **finish=true 时必须填写**,详细说明结束原因(任务完成/等待用户/信息不足/无法处理等)
78
+ 16. <next_step>: **finish=false 时必须填写**,描述下一步计划做什么,要求简洁明确(1-2句话)
79
+ 17. 使用中文输出所有内容
78
80
 
79
81
  ## 工具选择指南
80
82
  - **搜索信息**: 用 `web_search`(返回标题+URL+摘要),不要用 browser_open
@@ -261,6 +263,108 @@ class MainAgent(BaseAgent):
261
263
  logger.warning(f"[{task_id}] 记忆合并异常: {e}")
262
264
  return None
263
265
 
266
+ async def _save_knowledge_to_base(
267
+ self,
268
+ content: str,
269
+ session_id: str,
270
+ task_id: str,
271
+ ) -> bool:
272
+ """
273
+ 将 LLM 输出的 <knowledge> 内容追加到知识库文件。
274
+
275
+ 存储策略:
276
+ - 知识按会话 (session_id) 分文件存储
277
+ - 文件路径: {knowledge_base_dir}/auto_knowledge/{session_id}.md
278
+ - 每次追加时检查重复(TF-IDF 相似度 ≥ 0.9 视为重复,跳过)
279
+ - 追加时带有时间戳标记
280
+
281
+ Returns:
282
+ True 表示成功存储了新知识,False 表示跳过(重复)或失败
283
+ """
284
+ if not self.context_builder or not self.context_builder.knowledge_base_dir:
285
+ logger.debug(f"[{task_id}] 知识库未配置,跳过 knowledge 存储")
286
+ return False
287
+
288
+ from datetime import datetime
289
+ from pathlib import Path
290
+
291
+ kb_dir = Path(self.context_builder.knowledge_base_dir)
292
+ auto_kb_dir = kb_dir / "auto_knowledge"
293
+ auto_kb_dir.mkdir(parents=True, exist_ok=True)
294
+
295
+ # 使用 session_id 作为文件名(取前8位避免过长)
296
+ safe_session = session_id.replace("-", "")[:8] if session_id else "default"
297
+ kb_file = auto_kb_dir / f"{safe_session}.md"
298
+
299
+ now_str = datetime.now().strftime("%Y-%m-%d %H:%M")
300
+
301
+ # 检查重复:与已有文件内容做相似度比较
302
+ existing_content = ""
303
+ if kb_file.exists():
304
+ try:
305
+ existing_content = kb_file.read_text(encoding="utf-8")
306
+ except Exception:
307
+ existing_content = ""
308
+
309
+ if existing_content and content.strip():
310
+ # 简单去重:检查新知识是否已存在于文件中
311
+ # 使用逐行比对 + 关键词匹配
312
+ new_lines = [line.strip() for line in content.strip().split("\n") if line.strip()]
313
+ existing_lines = [line.strip() for line in existing_content.split("\n") if line.strip() and not line.strip().startswith("- [")]
314
+
315
+ dup_count = 0
316
+ for new_line in new_lines:
317
+ # 精确匹配或高度相似(共现字符占比 > 85%)
318
+ is_dup = False
319
+ for ex_line in existing_lines:
320
+ # 计算字符重叠率
321
+ set_new = set(new_line)
322
+ set_ex = set(ex_line)
323
+ if not set_new or not set_ex:
324
+ continue
325
+ overlap = len(set_new & set_ex) / max(len(set_new), len(set_ex))
326
+ if overlap >= 0.85 or new_line == ex_line:
327
+ is_dup = True
328
+ break
329
+ if is_dup:
330
+ dup_count += 1
331
+
332
+ if dup_count == len(new_lines):
333
+ logger.info(f"[{task_id}] 知识全部重复,跳过存储 ({dup_count}/{len(new_lines)} 条)")
334
+ return False
335
+ elif dup_count > 0:
336
+ # 过滤掉重复的行
337
+ filtered_lines = []
338
+ for new_line in new_lines:
339
+ is_dup = False
340
+ for ex_line in existing_lines:
341
+ set_new = set(new_line)
342
+ set_ex = set(ex_line)
343
+ if not set_new or not set_ex:
344
+ continue
345
+ overlap = len(set_new & set_ex) / max(len(set_new), len(set_ex))
346
+ if overlap >= 0.85 or new_line == ex_line:
347
+ is_dup = True
348
+ break
349
+ if not is_dup:
350
+ filtered_lines.append(new_line)
351
+ content = "\n".join(filtered_lines)
352
+ logger.info(f"[{task_id}] 知识去重: {dup_count}/{len(new_lines)} 条重复,{len(filtered_lines)} 条新增")
353
+
354
+ # 追加写入
355
+ try:
356
+ with open(kb_file, "a", encoding="utf-8") as f:
357
+ f.write(f"\n## {now_str}\n")
358
+ f.write(content.strip() + "\n")
359
+ logger.info(
360
+ f"[{task_id}] 知识已存入知识库: {kb_file.name} "
361
+ f"({len(content)} 字符, {len(content.strip().split(chr(10)))} 条)"
362
+ )
363
+ return True
364
+ except Exception as e:
365
+ logger.warning(f"[{task_id}] 知识写入失败: {e}")
366
+ return False
367
+
264
368
  async def process_v2(
265
369
  self,
266
370
  context: AgentContext,
@@ -402,6 +506,7 @@ class MainAgent(BaseAgent):
402
506
  task_plan=current_task_plan,
403
507
  agent_override_prompt=agent_override_prompt,
404
508
  get_knowledge=get_knowledge_content,
509
+ recall=recall_content,
405
510
  )
406
511
 
407
512
  await self._emit_v2_event(
@@ -517,77 +622,92 @@ class MainAgent(BaseAgent):
517
622
  if response_text:
518
623
  logger.debug(f"[{task_id}] 模型回复用户: {response_text[:100]}")
519
624
  context.working_memory["model_response"] = response_text
625
+ _v2_reasoning_collected.append(response_text)
520
626
  await self._emit_v2_event(
521
627
  "v2_reasoning",
522
628
  {"content": response_text},
523
629
  stream_callback,
524
630
  )
525
631
 
526
- # Step 6: 处理 remember — 查重+LLM合并后存入长期记忆
632
+ # Step 6: 处理 remember — 按 type 分全局/会话存储
527
633
  if parsed.remember:
528
634
  try:
529
635
  if self.memory:
530
- # 查找是否有相似记忆
531
- dup_memory = self.memory.find_duplicate_memory(
532
- content=parsed.remember,
533
- session_id=context.session_id,
534
- key="conversation_insight",
535
- )
536
- if dup_memory:
537
- # 发现相似记忆 → 调用 LLM API 合并新旧记忆
538
- logger.info(
539
- f"[{task_id}] 记忆查重: 发现相似内容,调用LLM合并 "
540
- f"(旧记忆ID={dup_memory.id}, 创建于={dup_memory.created_at})"
541
- )
542
- merged_content = await self._merge_duplicate_memory(
543
- old_memory=dup_memory,
544
- new_content=parsed.remember,
545
- context=context,
546
- task_id=task_id,
636
+ _rem_type = parsed.remember_type or "session"
637
+
638
+ if _rem_type == "global":
639
+ # === 全局记忆:查重 + LLM 合并 → add_global ===
640
+ dup_memory = self.memory.find_duplicate_memory(
641
+ content=parsed.remember,
642
+ session_id=context.session_id,
643
+ key="conversation_insight",
547
644
  )
548
- if merged_content:
549
- # 用 LLM 合并后的内容替换旧记忆
550
- self.memory.update_memory(
551
- memory_id=dup_memory.id,
552
- content=merged_content,
553
- summary=truncate_str(merged_content, 200),
645
+ if dup_memory:
646
+ logger.info(
647
+ f"[{task_id}] 全局记忆查重: 发现相似内容,调用LLM合并 "
648
+ f"(旧记忆ID={dup_memory.id}, 创建于={dup_memory.created_at})"
554
649
  )
555
- logger.info(f"[{task_id}] 记忆已合并更新: {dup_memory.id}")
556
- else:
557
- # LLM 合并失败,直接更新为新内容
558
- self.memory.update_memory(
559
- memory_id=dup_memory.id,
560
- content=parsed.remember,
561
- )
562
- logger.info(f"[{task_id}] 记忆直接更新为新内容: {dup_memory.id}")
563
- else:
564
- # 无重复,直接存储新记忆
565
- if self.memory_agent:
566
- mem_ctx = AgentContext(
650
+ merged_content = await self._merge_duplicate_memory(
651
+ old_memory=dup_memory,
652
+ new_content=parsed.remember,
653
+ context=context,
567
654
  task_id=task_id,
568
- session_id=context.session_id,
569
- metadata={
570
- "memory_action": "save",
571
- "content": parsed.remember,
572
- },
573
655
  )
574
- await self.memory_agent.process(mem_ctx)
656
+ if merged_content:
657
+ self.memory.update_memory(
658
+ memory_id=dup_memory.id,
659
+ content=merged_content,
660
+ summary=truncate_str(merged_content, 200),
661
+ )
662
+ logger.info(f"[{task_id}] 全局记忆已合并更新: {dup_memory.id}")
663
+ else:
664
+ self.memory.update_memory(
665
+ memory_id=dup_memory.id,
666
+ content=parsed.remember,
667
+ )
668
+ logger.info(f"[{task_id}] 全局记忆直接更新: {dup_memory.id}")
575
669
  else:
576
- self.memory.add_long_term(
670
+ self.memory.add_global(
577
671
  session_id=context.session_id,
578
672
  key="conversation_insight",
579
673
  content=parsed.remember,
580
674
  summary=truncate_str(parsed.remember, 200),
581
675
  importance=0.7,
582
676
  )
677
+ else:
678
+ # === 会话记忆:直接存储 → add_session ===
679
+ self.memory.add_session(
680
+ session_id=context.session_id,
681
+ key="conversation_insight",
682
+ content=parsed.remember,
683
+ importance=0.6,
684
+ )
685
+
583
686
  await self._emit_v2_event(
584
687
  "v2_memory_saved",
585
- {"content": truncate_str(parsed.remember, 200)},
688
+ {"type": _rem_type, "content": truncate_str(parsed.remember, 200)},
586
689
  stream_callback,
587
690
  )
588
691
  except Exception as e:
589
692
  logger.warning(f"[{task_id}] 存入记忆失败: {e}")
590
693
 
694
+ # Step 6.5: 处理 knowledge — 存入知识库文件
695
+ if parsed.knowledge:
696
+ try:
697
+ kb_saved = await self._save_knowledge_to_base(
698
+ content=parsed.knowledge,
699
+ session_id=context.session_id,
700
+ task_id=task_id,
701
+ )
702
+ if kb_saved:
703
+ await self._emit_v2_event(
704
+ "v2_knowledge_saved",
705
+ {"content": truncate_str(parsed.knowledge, 200)},
706
+ stream_callback,
707
+ )
708
+ except Exception as e:
709
+ logger.warning(f"[{task_id}] 存入知识库失败: {e}")
710
+
591
711
  # Step 7: 处理 recall — 记录下一轮需要检索的记忆内容
592
712
  if parsed.recall:
593
713
  recall_content = parsed.recall
@@ -660,6 +780,7 @@ class MainAgent(BaseAgent):
660
780
  # Step 11: 有工具调用 — 先执行所有工具,再根据 finish 决定回调
661
781
  need_callback = False
662
782
  tool_outputs_parts = []
783
+ _reasoning_len_before_round = len(_v2_reasoning_collected) # 记录本轮开始时的长度
663
784
 
664
785
  for tool_info in parsed.tools_to_call:
665
786
  tool_name = tool_info.get("toolname", "").strip()
@@ -706,38 +827,103 @@ class MainAgent(BaseAgent):
706
827
 
707
828
  # 发送工具结果事件
708
829
  # 提取实际输出:SkillResult 有 output/message/data,ExecResult 有 stdout/stderr
709
- def _format_data_for_llm(data):
710
- """将结构化 data 格式化为 LLM 可读的文本"""
711
- if data is None:
830
+ def _format_data_for_llm(data, _depth=0):
831
+ """将结构化 data 递归格式化为 LLM 可读的文本"""
832
+ _MAX_DEPTH = 3
833
+ _MAX_LIST_ITEMS = 50
834
+
835
+ if data is None or _depth > _MAX_DEPTH:
712
836
  return ""
713
837
  if isinstance(data, str):
714
838
  return data
839
+ if isinstance(data, (int, float, bool)):
840
+ return str(data)
841
+
842
+ if isinstance(data, list):
843
+ lines = []
844
+ for i, item in enumerate(data[:_MAX_LIST_ITEMS], 1):
845
+ if isinstance(item, dict):
846
+ # 优先提取名称类字段作为主标题
847
+ name = (
848
+ item.get("name") or item.get("title")
849
+ or item.get("text") or item.get("path")
850
+ or item.get("file") or item.get("url") or ""
851
+ )
852
+ # 其余字段作为详细信息
853
+ detail_parts = []
854
+ for k, v in item.items():
855
+ if k in ("name", "title", "text") and name:
856
+ continue
857
+ if v is None or v == "" or v == []:
858
+ continue
859
+ if isinstance(v, (list, dict)):
860
+ sub = _format_data_for_llm(v, _depth + 1)
861
+ if sub:
862
+ detail_parts.append(f"{k}={sub}")
863
+ else:
864
+ detail_parts.append(f"{k}={v}")
865
+ detail = ", ".join(detail_parts)
866
+ lines.append(
867
+ f"{i}. {name}" + (f" ({detail})" if detail else "")
868
+ )
869
+ elif isinstance(item, (list, dict)):
870
+ sub = _format_data_for_llm(item, _depth + 1)
871
+ lines.append(f"{i}. {sub}" if sub else f"{i}. (空)")
872
+ else:
873
+ lines.append(f"{i}. {item}")
874
+ if len(data) > _MAX_LIST_ITEMS:
875
+ lines.append(f"... 共 {len(data)} 项,仅显示前 {_MAX_LIST_ITEMS} 项")
876
+ return "\n".join(lines)
877
+
715
878
  if isinstance(data, dict):
716
879
  # 搜索结果列表格式 (web_search)
717
880
  results = data.get("results")
718
881
  if isinstance(results, list):
719
882
  lines = []
720
- for i, r in enumerate(results, 1):
721
- title = r.get("title", "")
722
- url = r.get("url", "")
723
- snippet = r.get("snippet", "")
724
- lines.append(f"{i}. {title}\n URL: {url}\n {snippet}")
883
+ for i, r in enumerate(results[:_MAX_LIST_ITEMS], 1):
884
+ if isinstance(r, dict):
885
+ title = r.get("title", "")
886
+ url = r.get("url", "")
887
+ snippet = r.get("snippet", "")
888
+ lines.append(
889
+ f"{i}. {title}\n URL: {url}\n {snippet}"
890
+ )
891
+ else:
892
+ lines.append(f"{i}. {r}")
725
893
  return "\n".join(lines)
894
+
726
895
  # 网页内容格式 (web_read)
727
896
  if "url" in data and "content" in data:
728
897
  title = data.get("title", "")
729
898
  content = data.get("content", "")
730
- lines = [f"标题: {title}", f"URL: {data['url']}", f"内容:\n{content}"]
899
+ lines = [
900
+ f"标题: {title}",
901
+ f"URL: {data['url']}",
902
+ f"内容:\n{content}",
903
+ ]
731
904
  return "\n".join(lines)
732
- # 通用 dict: key-value 格式
905
+
906
+ # 通用 dict: 递归格式化嵌套结构
733
907
  parts = []
734
908
  for k, v in data.items():
735
909
  if k == "results":
736
910
  continue # 已在上面处理
737
- parts.append(f"{k}: {v}")
911
+ if isinstance(v, list):
912
+ if len(v) == 0:
913
+ parts.append(f"{k}: (空)")
914
+ else:
915
+ sub = _format_data_for_llm(v, _depth + 1)
916
+ parts.append(f"{k}:\n{sub}")
917
+ elif isinstance(v, dict):
918
+ if not v:
919
+ parts.append(f"{k}: (空)")
920
+ else:
921
+ sub = _format_data_for_llm(v, _depth + 1)
922
+ parts.append(f"{k}:\n{sub}")
923
+ else:
924
+ parts.append(f"{k}: {v}")
738
925
  return "\n".join(parts) if parts else str(data)
739
- if isinstance(data, list):
740
- return "\n".join(str(item) for item in data)
926
+
741
927
  return str(data)
742
928
 
743
929
  def _extract_tool_output(tr):
@@ -790,8 +976,20 @@ class MainAgent(BaseAgent):
790
976
  need_callback = True
791
977
 
792
978
  output_str = tool_output_text
793
- # 搜索和网页读取类工具允许更长的输出
794
- _max_output = 6000 if tool_name in ("web_search", "web_read", "url_read") else 3000
979
+ # 数据密集型工具允许更长的输出
980
+ _HEAVY_TOOLS = ("web_search", "web_read", "url_read", "file_list",
981
+ "file_search", "browser_open", "process_list")
982
+ # OpenClaw prompt-only 技能也允许较长输出(SKILL.md 指令)
983
+ _is_openclaw = (
984
+ isinstance(tool_result.get("data"), dict)
985
+ and tool_result.get("data", {}).get("skill_type") == "openclaw"
986
+ )
987
+ if tool_name in _HEAVY_TOOLS:
988
+ _max_output = 6000
989
+ elif _is_openclaw:
990
+ _max_output = 8000
991
+ else:
992
+ _max_output = 3000
795
993
  tool_outputs_parts.append(
796
994
  f"### {before_call}\n"
797
995
  f"**工具**: {tool_name}\n"
@@ -831,8 +1029,15 @@ class MainAgent(BaseAgent):
831
1029
  # 核心逻辑: finish=true 表示任务已完成/不需要再调用LLM,即使工具设置了callback=true
832
1030
  if parsed.finish:
833
1031
  logger.info(f"[{task_id}] finish=true,任务已完成,不回调 LLM")
834
- # 构建有意义的最终回复:使用收集到的 reasoning text + 任务计划摘要
835
- if _v2_reasoning_collected:
1032
+ # 构建有意义的最终回复:使用当前轮次的 reasoning text + 任务计划摘要
1033
+ # 注意:之前回调时已保存前几轮的文本,这里只保存当前轮次新增的部分
1034
+ _current_round = _v2_reasoning_collected[_reasoning_len_before_round:]
1035
+ if _current_round:
1036
+ final_text = "\n".join(_current_round)
1037
+ if current_task_plan:
1038
+ final_text += f"\n\n{current_task_plan}"
1039
+ elif _v2_reasoning_collected:
1040
+ # 没有回调历史(第一轮就 finish),保存全部
836
1041
  final_text = "\n".join(_v2_reasoning_collected)
837
1042
  if current_task_plan:
838
1043
  final_text += f"\n\n{current_task_plan}"
@@ -853,7 +1058,13 @@ class MainAgent(BaseAgent):
853
1058
  # finish=false: 根据工具的 callback 标志决定是否回调
854
1059
  if not need_callback:
855
1060
  logger.info(f"[{task_id}] 所有工具无需回调且 finish=false,结束循环")
856
- if _v2_reasoning_collected:
1061
+ # 只保存当前轮次新增的文本(前几轮已通过回调保存)
1062
+ _current_round = _v2_reasoning_collected[_reasoning_len_before_round:]
1063
+ if _current_round:
1064
+ final_text = "\n".join(_current_round)
1065
+ if current_task_plan:
1066
+ final_text += f"\n\n{current_task_plan}"
1067
+ elif _v2_reasoning_collected:
857
1068
  final_text = "\n".join(_v2_reasoning_collected)
858
1069
  if current_task_plan:
859
1070
  final_text += f"\n\n{current_task_plan}"
@@ -873,6 +1084,30 @@ class MainAgent(BaseAgent):
873
1084
 
874
1085
  logger.info(f"[{task_id}] finish=false 且 need_callback=true,回调 LLM...")
875
1086
 
1087
+ # 回调前,保存当前轮次的 LLM 输出到短期记忆
1088
+ # 这样每轮工具调用都有对应的 assistant 消息记录
1089
+ if self.memory:
1090
+ _round_items = _v2_reasoning_collected[_reasoning_len_before_round:]
1091
+ if _round_items:
1092
+ _round_output = "\n".join(_round_items)
1093
+ if _round_output.strip():
1094
+ self.memory.add_short_term(
1095
+ session_id=context.session_id,
1096
+ role="assistant",
1097
+ content=_round_output,
1098
+ )
1099
+
1100
+ # 循环正常结束(max_iter 耗尽)时兜底保存
1101
+ else:
1102
+ if self.memory and _v2_reasoning_collected:
1103
+ _fallback_text = "\n".join(_v2_reasoning_collected)
1104
+ if _fallback_text.strip():
1105
+ self.memory.add_short_term(
1106
+ session_id=context.session_id,
1107
+ role="assistant",
1108
+ content=_fallback_text,
1109
+ )
1110
+
876
1111
  context.working_memory["iterations"] = self._iteration_count
877
1112
  if current_task_plan:
878
1113
  context.working_memory["task_plan"] = current_task_plan
@@ -22,9 +22,9 @@ class MemoryAgent(BaseAgent):
22
22
  记忆管理 Agent。
23
23
 
24
24
  职责:
25
- - 对话上下文管理(短期记忆)
26
- - 任务进度跟踪(工作记忆)
27
- - 知识/经验存储(长期记忆)
25
+ - 对话上下文管理(会话记忆 session)
26
+ - 任务进度跟踪(会话记忆 session)
27
+ - 知识/经验存储(全局记忆 global)
28
28
  - 记忆检索与总结
29
29
  - 错误模式记录(避免重复犯错)
30
30
  """
@@ -101,14 +101,14 @@ class MemoryAgent(BaseAgent):
101
101
  return context
102
102
 
103
103
  async def _save_conversation(self, context: AgentContext, session_id: str):
104
- """保存对话消息到短期记忆"""
104
+ """保存对话消息到会话记忆"""
105
105
  if not self.memory:
106
106
  return
107
107
 
108
108
  for msg in context.conversation_history:
109
109
  if msg.role in ("user", "assistant", "system", "tool"):
110
110
  # 避免重复保存
111
- self.memory.add_short_term(
111
+ self.memory.add_session(
112
112
  session_id=session_id,
113
113
  role=msg.role,
114
114
  content=msg.content,
@@ -119,7 +119,7 @@ class MemoryAgent(BaseAgent):
119
119
  max_msgs = self.config.memory.max_short_term
120
120
  self.memory.prune_conversation(session_id, max_msgs)
121
121
 
122
- logger.debug(f"对话已保存到短期记忆 (session={session_id})")
122
+ logger.debug(f"对话已保存到会话记忆 (session={session_id})")
123
123
 
124
124
  async def _get_context(self, context: AgentContext, session_id: str):
125
125
  """获取对话上下文"""
@@ -133,25 +133,27 @@ class MemoryAgent(BaseAgent):
133
133
  logger.debug(f"已加载对话上下文: {len(entries)} 条")
134
134
 
135
135
  async def _save_progress(self, context: AgentContext, session_id: str):
136
- """保存任务进度到工作记忆"""
136
+ """保存任务进度到会话记忆"""
137
137
  if not self.memory:
138
138
  return
139
139
 
140
140
  progress = context.metadata.get("progress_data", {})
141
141
  for key, value in progress.items() if isinstance(progress, dict) else []:
142
- self.memory.add_working(
142
+ self.memory.add_session(
143
143
  session_id=session_id,
144
144
  key=key,
145
145
  content=str(value) if not isinstance(value, str) else value,
146
+ importance=0.7,
146
147
  )
147
148
 
148
149
  # 也保存整体状态
149
150
  task_status = context.working_memory.get("task_status", "进行中")
150
- self.memory.add_working(
151
+ self.memory.add_session(
151
152
  session_id=session_id,
152
153
  key="task_status",
153
154
  content=task_status,
154
155
  metadata=context.working_memory,
156
+ importance=0.7,
155
157
  )
156
158
  logger.debug(f"工作进度已保存 (session={session_id})")
157
159
 
@@ -230,7 +232,7 @@ class MemoryAgent(BaseAgent):
230
232
  # 保存偏好
231
233
  preferences = result.get("preferences", {})
232
234
  for key, value in preferences.items():
233
- self.memory.add_long_term(
235
+ self.memory.add_global(
234
236
  session_id="global",
235
237
  key="user_pref",
236
238
  content=f"{key}: {value}",
@@ -278,7 +280,7 @@ class MemoryAgent(BaseAgent):
278
280
  pref_key = context.metadata.get("pref_key", "")
279
281
  pref_value = context.metadata.get("pref_value", "")
280
282
  if pref_key and pref_value:
281
- self.memory.add_long_term(
283
+ self.memory.add_global(
282
284
  session_id="global",
283
285
  key="user_pref",
284
286
  content=f"{pref_key}: {pref_value}",
@@ -293,9 +295,9 @@ class MemoryAgent(BaseAgent):
293
295
 
294
296
  query = context.user_message
295
297
 
296
- # 搜索长期记忆
297
- long_term = self.memory.search(
298
- query, session_id="", category="long_term", limit=5
298
+ # 搜索全局记忆
299
+ global_memories = self.memory.search(
300
+ query, session_id="", category="global", limit=5
299
301
  )
300
302
  # 搜索错误模式
301
303
  errors = self.memory.get_error_patterns(session_id="global", limit=3)
@@ -303,9 +305,9 @@ class MemoryAgent(BaseAgent):
303
305
  prefs = self.memory.get_preferences(session_id="global")
304
306
 
305
307
  context.working_memory["relevant_memories"] = {
306
- "long_term": [
308
+ "global": [
307
309
  {"content": e.content[:500], "key": e.key, "summary": e.summary}
308
- for e in long_term
310
+ for e in global_memories
309
311
  ],
310
312
  "error_patterns": [
311
313
  {"content": e.content[:300]} for e in errors
@@ -325,9 +327,9 @@ class MemoryAgent(BaseAgent):
325
327
  context_parts.append("\n## 历史错误(避免重复)")
326
328
  context_parts.extend(f"- {e.content[:200]}" for e in errors)
327
329
 
328
- if long_term:
330
+ if global_memories:
329
331
  context_parts.append("\n## 相关经验")
330
- context_parts.extend(f"- {e.summary or e.content[:200]}" for e in long_term)
332
+ context_parts.extend(f"- {e.summary or e.content[:200]}" for e in global_memories)
331
333
 
332
334
  if context_parts:
333
335
  context.working_memory["memory_context_prompt"] = "\n".join(context_parts)
package/config.py CHANGED
@@ -45,11 +45,16 @@ class LLMConfig:
45
45
  class MemoryConfig:
46
46
  """记忆系统配置"""
47
47
  db_path: str = "" # SQLite 数据库路径,默认 ~/.myagent/memory.db
48
- max_short_term: int = 50 # 短期记忆最大轮数
49
- max_working: int = 100 # 工作记忆最大条数
48
+ max_session: int = 50 # 会话记忆最大对话轮数
49
+ max_working: int = 100 # 任务进度最大条数
50
50
  auto_summarize: bool = True # 自动总结开关
51
51
  summarize_threshold: int = 20 # 触发总结的对话轮数
52
52
 
53
+ # 兼容旧配置名
54
+ @property
55
+ def max_short_term(self) -> int:
56
+ return self.max_session
57
+
53
58
 
54
59
  @dataclass
55
60
  class ExecutorConfig: