myagent-ai 1.10.6 → 1.10.8

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,
@@ -349,9 +453,9 @@ class MainAgent(BaseAgent):
349
453
  except Exception as e:
350
454
  logger.warning(f"[{task_id}] 加载历史对话失败: {e}")
351
455
 
352
- # 保存用户消息到短期记忆
456
+ # 保存用户消息到会话记忆
353
457
  if self.memory:
354
- self.memory.add_short_term(
458
+ self.memory.add_session(
355
459
  session_id=context.session_id,
356
460
  role="user",
357
461
  content=context.user_message,
@@ -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(
@@ -446,9 +551,9 @@ class MainAgent(BaseAgent):
446
551
  llm_raw = response.content
447
552
  logger.debug(f"[{task_id}] LLM 输出 (前500字): {llm_raw[:500]}")
448
553
 
449
- # 保存 LLM 原始输出到短期记忆(用于回溯和审计)
554
+ # 保存 LLM 原始输出到会话记忆(用于回溯和审计)
450
555
  if self.memory:
451
- self.memory.add_short_term(
556
+ self.memory.add_session(
452
557
  session_id=context.session_id,
453
558
  role="assistant",
454
559
  content=llm_raw,
@@ -483,7 +588,7 @@ class MainAgent(BaseAgent):
483
588
  context.working_memory["final_response"] = final_text
484
589
  await self._emit_v2_event("v2_reasoning", {"content": final_text}, stream_callback)
485
590
  if self.memory:
486
- self.memory.add_short_term(
591
+ self.memory.add_session(
487
592
  session_id=context.session_id,
488
593
  role="assistant",
489
594
  content=final_text,
@@ -496,7 +601,7 @@ class MainAgent(BaseAgent):
496
601
  context.working_memory["final_response"] = final_text
497
602
  await self._emit_v2_event("v2_reasoning", {"content": final_text}, stream_callback)
498
603
  if self.memory:
499
- self.memory.add_short_term(
604
+ self.memory.add_session(
500
605
  session_id=context.session_id,
501
606
  role="assistant",
502
607
  content=final_text,
@@ -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})"
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",
541
644
  )
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,
547
- )
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),
554
- )
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,
645
+ if dup_memory:
646
+ logger.info(
647
+ f"[{task_id}] 全局记忆查重: 发现相似内容,调用LLM合并 "
648
+ f"(旧记忆ID={dup_memory.id}, 创建于={dup_memory.created_at})"
561
649
  )
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
@@ -621,7 +741,7 @@ class MainAgent(BaseAgent):
621
741
  stream_callback,
622
742
  )
623
743
  if self.memory:
624
- self.memory.add_short_term(
744
+ self.memory.add_session(
625
745
  session_id=context.session_id,
626
746
  role="assistant",
627
747
  content=parsed.ask_user,
@@ -638,7 +758,7 @@ class MainAgent(BaseAgent):
638
758
  context.working_memory["final_response"] = final_text
639
759
  await self._emit_v2_event("v2_reasoning", {"content": truncate_str(final_text, 3000)}, stream_callback)
640
760
  if self.memory:
641
- self.memory.add_short_term(
761
+ self.memory.add_session(
642
762
  session_id=context.session_id,
643
763
  role="assistant",
644
764
  content=final_text,
@@ -650,7 +770,7 @@ class MainAgent(BaseAgent):
650
770
  context.working_memory["final_response"] = final_text
651
771
  await self._emit_v2_event("v2_reasoning", {"content": truncate_str(final_text, 3000)}, stream_callback)
652
772
  if self.memory:
653
- self.memory.add_short_term(
773
+ self.memory.add_session(
654
774
  session_id=context.session_id,
655
775
  role="assistant",
656
776
  content=final_text,
@@ -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"
@@ -808,16 +1006,16 @@ class MainAgent(BaseAgent):
808
1006
  content=f"[工具 {tool_name} 执行完成] {'成功' if tool_result.get('success') else '失败'}",
809
1007
  ))
810
1008
 
811
- # 保存工具调用到短期记忆
1009
+ # 保存工具调用到会话记忆
812
1010
  if self.memory:
813
- self.memory.add_short_term(
1011
+ self.memory.add_session(
814
1012
  session_id=context.session_id,
815
1013
  role="assistant",
816
1014
  content=f"调用工具: {tool_name}\n参数: {truncate_str(parms, 1000)}",
817
1015
  key="tool_call",
818
1016
  importance=0.4,
819
1017
  )
820
- self.memory.add_short_term(
1018
+ self.memory.add_session(
821
1019
  session_id=context.session_id,
822
1020
  role="tool",
823
1021
  content=f"[{tool_name}] {'成功' if tool_result.get('success') else '失败'}\n{truncate_str(output_str, 5000)}",
@@ -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}"
@@ -843,7 +1048,7 @@ class MainAgent(BaseAgent):
843
1048
  context.working_memory["final_response"] = final_text
844
1049
  await self._emit_v2_event("v2_reasoning", {"content": truncate_str(final_text, 3000)}, stream_callback)
845
1050
  if self.memory:
846
- self.memory.add_short_term(
1051
+ self.memory.add_session(
847
1052
  session_id=context.session_id,
848
1053
  role="assistant",
849
1054
  content=final_text,
@@ -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}"
@@ -864,7 +1075,7 @@ class MainAgent(BaseAgent):
864
1075
  context.working_memory["final_response"] = final_text
865
1076
  await self._emit_v2_event("v2_reasoning", {"content": truncate_str(final_text, 3000)}, stream_callback)
866
1077
  if self.memory:
867
- self.memory.add_short_term(
1078
+ self.memory.add_session(
868
1079
  session_id=context.session_id,
869
1080
  role="assistant",
870
1081
  content=final_text,
@@ -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_session(
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_session(
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