|
@@ -28,7 +28,7 @@ from agent.trace.protocols import TraceStore
|
|
|
from agent.trace.goal_models import GoalTree
|
|
from agent.trace.goal_models import GoalTree
|
|
|
from agent.trace.compaction import (
|
|
from agent.trace.compaction import (
|
|
|
CompressionConfig,
|
|
CompressionConfig,
|
|
|
- filter_by_goal_status,
|
|
|
|
|
|
|
+ compress_completed_goals,
|
|
|
estimate_tokens,
|
|
estimate_tokens,
|
|
|
needs_level2_compression,
|
|
needs_level2_compression,
|
|
|
build_compression_prompt,
|
|
build_compression_prompt,
|
|
@@ -105,6 +105,7 @@ class RunConfig:
|
|
|
max_iterations: int = 200
|
|
max_iterations: int = 200
|
|
|
tools: Optional[List[str]] = None # None = 全部已注册工具
|
|
tools: Optional[List[str]] = None # None = 全部已注册工具
|
|
|
side_branch_max_turns: int = 5 # 侧分支最大轮次(压缩/反思)
|
|
side_branch_max_turns: int = 5 # 侧分支最大轮次(压缩/反思)
|
|
|
|
|
+ goal_compression: Literal["none", "on_complete", "on_overflow"] = "on_overflow" # Goal 压缩模式
|
|
|
|
|
|
|
|
# --- 强制侧分支(用于 API 手动触发或自动压缩流程)---
|
|
# --- 强制侧分支(用于 API 手动触发或自动压缩流程)---
|
|
|
# 使用列表作为侧分支队列,每次完成一个侧分支后 pop(0) 取下一个
|
|
# 使用列表作为侧分支队列,每次完成一个侧分支后 pop(0) 取下一个
|
|
@@ -787,19 +788,20 @@ class AgentRunner:
|
|
|
config.force_side_branch = ["reflection", "compression"]
|
|
config.force_side_branch = ["reflection", "compression"]
|
|
|
return history, head_seq, sequence, True
|
|
return history, head_seq, sequence, True
|
|
|
|
|
|
|
|
- # Level 1 压缩:GoalTree 过滤
|
|
|
|
|
- if self.trace_store and goal_tree:
|
|
|
|
|
|
|
+ # 以下为未启用反思、需要压缩的情况,直接进行level 1压缩,并检查是否需要进行level 2压缩(进入侧分支)
|
|
|
|
|
+ # Level 1 压缩:Goal 完成压缩
|
|
|
|
|
+ if config.goal_compression != "none" and self.trace_store and goal_tree:
|
|
|
if head_seq > 0:
|
|
if head_seq > 0:
|
|
|
main_path_msgs = await self.trace_store.get_main_path_messages(
|
|
main_path_msgs = await self.trace_store.get_main_path_messages(
|
|
|
trace_id, head_seq
|
|
trace_id, head_seq
|
|
|
)
|
|
)
|
|
|
- filtered_msgs = filter_by_goal_status(main_path_msgs, goal_tree)
|
|
|
|
|
- if len(filtered_msgs) < len(main_path_msgs):
|
|
|
|
|
|
|
+ compressed_msgs = compress_completed_goals(main_path_msgs, goal_tree)
|
|
|
|
|
+ if len(compressed_msgs) < len(main_path_msgs):
|
|
|
logger.info(
|
|
logger.info(
|
|
|
"Level 1 压缩: %d -> %d 条消息",
|
|
"Level 1 压缩: %d -> %d 条消息",
|
|
|
- len(main_path_msgs), len(filtered_msgs),
|
|
|
|
|
|
|
+ len(main_path_msgs), len(compressed_msgs),
|
|
|
)
|
|
)
|
|
|
- history = [msg.to_llm_dict() for msg in filtered_msgs]
|
|
|
|
|
|
|
+ history = [msg.to_llm_dict() for msg in compressed_msgs]
|
|
|
else:
|
|
else:
|
|
|
logger.info(
|
|
logger.info(
|
|
|
"Level 1 压缩: 无可过滤消息 (%d 条全部保留)",
|
|
"Level 1 压缩: 无可过滤消息 (%d 条全部保留)",
|
|
@@ -807,7 +809,7 @@ class AgentRunner:
|
|
|
)
|
|
)
|
|
|
elif needs_compression:
|
|
elif needs_compression:
|
|
|
logger.warning(
|
|
logger.warning(
|
|
|
- "消息数 (%d) 或 token 数 (%d) 超过阈值,但无法执行 Level 1 压缩(缺少 store 或 goal_tree)",
|
|
|
|
|
|
|
+ "消息数 (%d) 或 token 数 (%d) 超过阈值,但无法执行 Level 1 压缩(缺少 store 或 goal_tree,或 goal_compression=none)",
|
|
|
msg_count, token_count,
|
|
msg_count, token_count,
|
|
|
)
|
|
)
|
|
|
|
|
|
|
@@ -853,15 +855,15 @@ class AgentRunner:
|
|
|
history: List[Dict],
|
|
history: List[Dict],
|
|
|
goal_tree: Optional[GoalTree],
|
|
goal_tree: Optional[GoalTree],
|
|
|
config: RunConfig,
|
|
config: RunConfig,
|
|
|
- sequence: int,
|
|
|
|
|
- start_head_seq: int,
|
|
|
|
|
- ) -> Tuple[List[Dict], int, int]:
|
|
|
|
|
- """单次 LLM 调用压缩(fallback 方案)"""
|
|
|
|
|
|
|
+ ) -> str:
|
|
|
|
|
+ """单次 LLM 调用生成压缩摘要,返回 summary 文本"""
|
|
|
|
|
|
|
|
- logger.info("执行单次 LLM 压缩(fallback)")
|
|
|
|
|
|
|
+ logger.info("执行单次 LLM 压缩")
|
|
|
|
|
|
|
|
- # 构建压缩 prompt
|
|
|
|
|
- compress_prompt = build_compression_prompt(goal_tree)
|
|
|
|
|
|
|
+ # 构建压缩 prompt(使用 SINGLE_TURN_PROMPT)
|
|
|
|
|
+ from agent.core.prompts import build_single_turn_prompt
|
|
|
|
|
+ goal_prompt = goal_tree.to_prompt(include_summary=True) if goal_tree else ""
|
|
|
|
|
+ compress_prompt = build_single_turn_prompt(goal_prompt)
|
|
|
compress_messages = list(history) + [
|
|
compress_messages = list(history) + [
|
|
|
{"role": "user", "content": compress_prompt}
|
|
{"role": "user", "content": compress_prompt}
|
|
|
]
|
|
]
|
|
@@ -888,32 +890,7 @@ class AgentRunner:
|
|
|
summary_text.index("[[SUMMARY]]") + len("[[SUMMARY]]"):
|
|
summary_text.index("[[SUMMARY]]") + len("[[SUMMARY]]"):
|
|
|
].strip()
|
|
].strip()
|
|
|
|
|
|
|
|
- if not summary_text:
|
|
|
|
|
- logger.warning("单次压缩未返回有效内容,跳过压缩")
|
|
|
|
|
- return history, start_head_seq, sequence
|
|
|
|
|
-
|
|
|
|
|
- # 创建 summary 消息
|
|
|
|
|
- from agent.core.prompts import build_summary_header
|
|
|
|
|
- summary_msg = Message.create(
|
|
|
|
|
- trace_id=trace_id,
|
|
|
|
|
- role="user",
|
|
|
|
|
- sequence=sequence,
|
|
|
|
|
- parent_sequence=start_head_seq,
|
|
|
|
|
- branch_type=None, # 主路径
|
|
|
|
|
- content=build_summary_header(summary_text),
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- if self.trace_store:
|
|
|
|
|
- await self.trace_store.add_message(summary_msg)
|
|
|
|
|
-
|
|
|
|
|
- new_history = self._rebuild_history_after_compression(
|
|
|
|
|
- history, summary_msg.to_llm_dict(), label="单次压缩"
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- new_head_seq = sequence
|
|
|
|
|
- sequence += 1
|
|
|
|
|
-
|
|
|
|
|
- return new_history, new_head_seq, sequence
|
|
|
|
|
|
|
+ return summary_text
|
|
|
|
|
|
|
|
async def _agent_loop(
|
|
async def _agent_loop(
|
|
|
self,
|
|
self,
|
|
@@ -937,13 +914,14 @@ class AgentRunner:
|
|
|
if trace.context.get("active_side_branch"):
|
|
if trace.context.get("active_side_branch"):
|
|
|
side_branch_data = trace.context["active_side_branch"]
|
|
side_branch_data = trace.context["active_side_branch"]
|
|
|
branch_id = side_branch_data["branch_id"]
|
|
branch_id = side_branch_data["branch_id"]
|
|
|
|
|
+ start_sequence = side_branch_data["start_sequence"]
|
|
|
|
|
|
|
|
- # 从数据库查询侧分支消息
|
|
|
|
|
|
|
+ # 从数据库查询侧分支消息(按 sequence 范围)
|
|
|
if self.trace_store:
|
|
if self.trace_store:
|
|
|
all_messages = await self.trace_store.get_trace_messages(trace_id)
|
|
all_messages = await self.trace_store.get_trace_messages(trace_id)
|
|
|
side_messages = [
|
|
side_messages = [
|
|
|
m for m in all_messages
|
|
m for m in all_messages
|
|
|
- if m.branch_id == branch_id
|
|
|
|
|
|
|
+ if m.sequence >= start_sequence
|
|
|
]
|
|
]
|
|
|
|
|
|
|
|
# 恢复侧分支上下文
|
|
# 恢复侧分支上下文
|
|
@@ -969,6 +947,8 @@ class AgentRunner:
|
|
|
# 重新计算 start_history_length
|
|
# 重新计算 start_history_length
|
|
|
side_branch_ctx.start_history_length = len(history) - len(side_messages)
|
|
side_branch_ctx.start_history_length = len(history) - len(side_messages)
|
|
|
|
|
|
|
|
|
|
+ break_after_side_branch = False # 侧分支退出后是否 break 主循环
|
|
|
|
|
+
|
|
|
for iteration in range(config.max_iterations):
|
|
for iteration in range(config.max_iterations):
|
|
|
# 更新活动时间(表明trace正在活跃运行)
|
|
# 更新活动时间(表明trace正在活跃运行)
|
|
|
if self.trace_store:
|
|
if self.trace_store:
|
|
@@ -1002,7 +982,11 @@ class AgentRunner:
|
|
|
# Context 管理(仅主路径)
|
|
# Context 管理(仅主路径)
|
|
|
needs_enter_side_branch = False
|
|
needs_enter_side_branch = False
|
|
|
if not side_branch_ctx:
|
|
if not side_branch_ctx:
|
|
|
- # 检查是否强制进入侧分支(API 手动触发)
|
|
|
|
|
|
|
+ # 侧分支退出后需要 break 主循环
|
|
|
|
|
+ if break_after_side_branch and not config.force_side_branch:
|
|
|
|
|
+ break
|
|
|
|
|
+
|
|
|
|
|
+ # 检查是否强制进入侧分支(API 手动触发或自动压缩流程)
|
|
|
if config.force_side_branch:
|
|
if config.force_side_branch:
|
|
|
needs_enter_side_branch = True
|
|
needs_enter_side_branch = True
|
|
|
logger.info(f"强制进入侧分支: {config.force_side_branch}")
|
|
logger.info(f"强制进入侧分支: {config.force_side_branch}")
|
|
@@ -1198,118 +1182,78 @@ class AgentRunner:
|
|
|
if side_branch_ctx:
|
|
if side_branch_ctx:
|
|
|
# 计算侧分支已执行的轮次
|
|
# 计算侧分支已执行的轮次
|
|
|
turns_in_branch = iteration - side_branch_ctx.start_iteration
|
|
turns_in_branch = iteration - side_branch_ctx.start_iteration
|
|
|
|
|
+ should_exit = turns_in_branch >= side_branch_ctx.max_turns or not tool_calls
|
|
|
|
|
|
|
|
- # 检查是否达到最大轮次
|
|
|
|
|
if turns_in_branch >= side_branch_ctx.max_turns:
|
|
if turns_in_branch >= side_branch_ctx.max_turns:
|
|
|
logger.warning(
|
|
logger.warning(
|
|
|
f"侧分支 {side_branch_ctx.type} 达到最大轮次 "
|
|
f"侧分支 {side_branch_ctx.type} 达到最大轮次 "
|
|
|
f"{side_branch_ctx.max_turns},强制退出"
|
|
f"{side_branch_ctx.max_turns},强制退出"
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- if side_branch_ctx.type == "compression":
|
|
|
|
|
- # 压缩侧分支:fallback 到单次 LLM 调用
|
|
|
|
|
- logger.info("Fallback 到单次 LLM 压缩")
|
|
|
|
|
-
|
|
|
|
|
- # 清除侧分支状态
|
|
|
|
|
- trace.context.pop("active_side_branch", None)
|
|
|
|
|
- if self.trace_store:
|
|
|
|
|
- await self.trace_store.update_trace(
|
|
|
|
|
- trace_id, context=trace.context
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- # 恢复到侧分支开始前的 history
|
|
|
|
|
- if self.trace_store:
|
|
|
|
|
- main_path_messages = await self.trace_store.get_main_path_messages(
|
|
|
|
|
- trace_id, side_branch_ctx.start_head_seq
|
|
|
|
|
- )
|
|
|
|
|
- history = [m.to_llm_dict() for m in main_path_messages]
|
|
|
|
|
-
|
|
|
|
|
- # 执行单次 LLM 压缩
|
|
|
|
|
- history, head_seq, sequence = await self._single_turn_compress(
|
|
|
|
|
- trace_id, history, goal_tree, config, sequence,
|
|
|
|
|
- side_branch_ctx.start_head_seq
|
|
|
|
|
|
|
+ if should_exit and side_branch_ctx.type == "compression":
|
|
|
|
|
+ # === 压缩侧分支退出(超时 + 正常完成统一处理)===
|
|
|
|
|
+ summary_text = ""
|
|
|
|
|
+
|
|
|
|
|
+ # 1. 从当前回复提取
|
|
|
|
|
+ if response_content:
|
|
|
|
|
+ if "[[SUMMARY]]" in response_content:
|
|
|
|
|
+ summary_text = response_content[
|
|
|
|
|
+ response_content.index("[[SUMMARY]]") + len("[[SUMMARY]]"):
|
|
|
|
|
+ ].strip()
|
|
|
|
|
+ elif response_content.strip():
|
|
|
|
|
+ summary_text = response_content.strip()
|
|
|
|
|
+
|
|
|
|
|
+ # 2. 从持久化存储按 sequence 范围查询
|
|
|
|
|
+ if not summary_text and self.trace_store:
|
|
|
|
|
+ all_messages = await self.trace_store.get_trace_messages(trace_id)
|
|
|
|
|
+ side_messages = [
|
|
|
|
|
+ m for m in all_messages
|
|
|
|
|
+ if m.sequence >= side_branch_ctx.start_sequence
|
|
|
|
|
+ ]
|
|
|
|
|
+ for msg in reversed(side_messages):
|
|
|
|
|
+ if msg.role == "assistant" and isinstance(msg.content, dict):
|
|
|
|
|
+ text = msg.content.get("text", "")
|
|
|
|
|
+ if "[[SUMMARY]]" in text:
|
|
|
|
|
+ summary_text = text[text.index("[[SUMMARY]]") + len("[[SUMMARY]]"):].strip()
|
|
|
|
|
+ break
|
|
|
|
|
+ elif text:
|
|
|
|
|
+ summary_text = text
|
|
|
|
|
+ break
|
|
|
|
|
+
|
|
|
|
|
+ # 3. 单次 LLM 调用
|
|
|
|
|
+ if not summary_text:
|
|
|
|
|
+ logger.warning("侧分支未生成有效 summary,fallback 到单次 LLM 压缩")
|
|
|
|
|
+ pre_branch_history = history[:side_branch_ctx.start_history_length]
|
|
|
|
|
+ summary_text = await self._single_turn_compress(
|
|
|
|
|
+ trace_id, pre_branch_history, goal_tree, config,
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- # 清除强制侧分支配置
|
|
|
|
|
- config.force_side_branch = None
|
|
|
|
|
-
|
|
|
|
|
- side_branch_ctx = None
|
|
|
|
|
- continue
|
|
|
|
|
-
|
|
|
|
|
- elif side_branch_ctx.type == "reflection":
|
|
|
|
|
- # 反思侧分支:直接退出,不管结果
|
|
|
|
|
- logger.info("反思侧分支超时,直接退出")
|
|
|
|
|
-
|
|
|
|
|
- # 清除侧分支状态
|
|
|
|
|
- trace.context.pop("active_side_branch", None)
|
|
|
|
|
-
|
|
|
|
|
- # 队列中如果还有侧分支,保持 force_side_branch;否则清空
|
|
|
|
|
- if not config.force_side_branch or len(config.force_side_branch) == 0:
|
|
|
|
|
- config.force_side_branch = None
|
|
|
|
|
- logger.info("反思超时,队列为空")
|
|
|
|
|
-
|
|
|
|
|
- if self.trace_store:
|
|
|
|
|
- await self.trace_store.update_trace(
|
|
|
|
|
- trace_id, context=trace.context
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- # 恢复到侧分支开始前的 history
|
|
|
|
|
- if self.trace_store:
|
|
|
|
|
- main_path_messages = await self.trace_store.get_main_path_messages(
|
|
|
|
|
- trace_id, side_branch_ctx.start_head_seq
|
|
|
|
|
- )
|
|
|
|
|
- history = [m.to_llm_dict() for m in main_path_messages]
|
|
|
|
|
- head_seq = side_branch_ctx.start_head_seq
|
|
|
|
|
-
|
|
|
|
|
- # 清除强制侧分支配置
|
|
|
|
|
- config.force_side_branch = None
|
|
|
|
|
-
|
|
|
|
|
- side_branch_ctx = None
|
|
|
|
|
- continue
|
|
|
|
|
-
|
|
|
|
|
- # 检查是否无工具调用(侧分支完成)
|
|
|
|
|
- if not tool_calls:
|
|
|
|
|
- logger.info(f"侧分支 {side_branch_ctx.type} 完成(无工具调用)")
|
|
|
|
|
-
|
|
|
|
|
- # 提取结果
|
|
|
|
|
- if side_branch_ctx.type == "compression":
|
|
|
|
|
- # 从数据库查询侧分支消息并提取 summary
|
|
|
|
|
- summary_text = ""
|
|
|
|
|
- if self.trace_store:
|
|
|
|
|
- all_messages = await self.trace_store.get_trace_messages(trace_id)
|
|
|
|
|
- side_messages = [
|
|
|
|
|
- m for m in all_messages
|
|
|
|
|
- if m.branch_id == side_branch_ctx.branch_id
|
|
|
|
|
- ]
|
|
|
|
|
-
|
|
|
|
|
- for msg in side_messages:
|
|
|
|
|
- if msg.role == "assistant" and isinstance(msg.content, dict):
|
|
|
|
|
- text = msg.content.get("text", "")
|
|
|
|
|
- if "[[SUMMARY]]" in text:
|
|
|
|
|
- summary_text = text[text.index("[[SUMMARY]]") + len("[[SUMMARY]]"):].strip()
|
|
|
|
|
- break
|
|
|
|
|
- elif text:
|
|
|
|
|
- summary_text = text
|
|
|
|
|
-
|
|
|
|
|
- if not summary_text:
|
|
|
|
|
- logger.warning("侧分支未生成有效 summary,使用默认")
|
|
|
|
|
- summary_text = "压缩完成"
|
|
|
|
|
-
|
|
|
|
|
- # 创建主路径的 summary 消息(末尾追加详细 GoalTree)
|
|
|
|
|
|
|
+ # 创建主路径 summary 消息并重建 history
|
|
|
|
|
+ if summary_text:
|
|
|
from agent.core.prompts import build_summary_header
|
|
from agent.core.prompts import build_summary_header
|
|
|
summary_content = build_summary_header(summary_text)
|
|
summary_content = build_summary_header(summary_text)
|
|
|
|
|
|
|
|
- # 追加详细 GoalTree(压缩后立即注入)
|
|
|
|
|
if goal_tree and goal_tree.goals:
|
|
if goal_tree and goal_tree.goals:
|
|
|
goal_tree_detail = goal_tree.to_prompt(include_summary=True)
|
|
goal_tree_detail = goal_tree.to_prompt(include_summary=True)
|
|
|
summary_content += f"\n\n## Current Plan\n\n{goal_tree_detail}"
|
|
summary_content += f"\n\n## Current Plan\n\n{goal_tree_detail}"
|
|
|
|
|
|
|
|
|
|
+ # 找第一条 user message 的 sequence 作为 parent
|
|
|
|
|
+ # 续跑时 get_main_path_messages 沿 parent 链回溯,
|
|
|
|
|
+ # 指向 first_user 可以跳过所有被压缩的中间消息
|
|
|
|
|
+ first_user_seq = None
|
|
|
|
|
+ if self.trace_store:
|
|
|
|
|
+ all_msgs = await self.trace_store.get_trace_messages(trace_id)
|
|
|
|
|
+ for m in all_msgs:
|
|
|
|
|
+ if m.role == "user":
|
|
|
|
|
+ first_user_seq = m.sequence
|
|
|
|
|
+ break
|
|
|
|
|
+
|
|
|
summary_msg = Message.create(
|
|
summary_msg = Message.create(
|
|
|
trace_id=trace_id,
|
|
trace_id=trace_id,
|
|
|
role="user",
|
|
role="user",
|
|
|
sequence=sequence,
|
|
sequence=sequence,
|
|
|
- parent_sequence=side_branch_ctx.start_head_seq,
|
|
|
|
|
- branch_type=None, # 回到主路径
|
|
|
|
|
|
|
+ parent_sequence=first_user_seq,
|
|
|
|
|
+ branch_type=None,
|
|
|
content=summary_content,
|
|
content=summary_content,
|
|
|
)
|
|
)
|
|
|
|
|
|
|
@@ -1319,41 +1263,42 @@ class AgentRunner:
|
|
|
history = self._rebuild_history_after_compression(
|
|
history = self._rebuild_history_after_compression(
|
|
|
history, summary_msg.to_llm_dict(), label="压缩侧分支"
|
|
history, summary_msg.to_llm_dict(), label="压缩侧分支"
|
|
|
)
|
|
)
|
|
|
-
|
|
|
|
|
head_seq = sequence
|
|
head_seq = sequence
|
|
|
sequence += 1
|
|
sequence += 1
|
|
|
|
|
+ else:
|
|
|
|
|
+ logger.error("所有压缩方案均未生成有效 summary,跳过压缩")
|
|
|
|
|
|
|
|
- # 清除侧分支队列
|
|
|
|
|
- config.force_side_branch = None
|
|
|
|
|
-
|
|
|
|
|
- elif side_branch_ctx.type == "reflection":
|
|
|
|
|
- # 反思侧分支:直接恢复主路径
|
|
|
|
|
- logger.info("反思侧分支完成")
|
|
|
|
|
|
|
+ # 清理
|
|
|
|
|
+ trace.context.pop("active_side_branch", None)
|
|
|
|
|
+ config.force_side_branch = None
|
|
|
|
|
+ if self.trace_store:
|
|
|
|
|
+ await self.trace_store.update_trace(
|
|
|
|
|
+ trace_id, context=trace.context, head_sequence=head_seq,
|
|
|
|
|
+ )
|
|
|
|
|
+ side_branch_ctx = None
|
|
|
|
|
+ continue
|
|
|
|
|
|
|
|
- if self.trace_store:
|
|
|
|
|
- main_path_messages = await self.trace_store.get_main_path_messages(
|
|
|
|
|
- trace_id, side_branch_ctx.start_head_seq
|
|
|
|
|
- )
|
|
|
|
|
- history = [m.to_llm_dict() for m in main_path_messages]
|
|
|
|
|
- head_seq = side_branch_ctx.start_head_seq
|
|
|
|
|
|
|
+ elif should_exit and side_branch_ctx.type == "reflection":
|
|
|
|
|
+ # === 反思侧分支退出(超时 + 正常完成统一处理)===
|
|
|
|
|
+ logger.info("反思侧分支退出")
|
|
|
|
|
|
|
|
- # 队列中如果还有侧分支,保持 force_side_branch;否则清空
|
|
|
|
|
- if not config.force_side_branch or len(config.force_side_branch) == 0:
|
|
|
|
|
- config.force_side_branch = None
|
|
|
|
|
- logger.info("反思完成,队列为空")
|
|
|
|
|
|
|
+ # 恢复主路径
|
|
|
|
|
+ if self.trace_store:
|
|
|
|
|
+ main_path_messages = await self.trace_store.get_main_path_messages(
|
|
|
|
|
+ trace_id, side_branch_ctx.start_head_seq
|
|
|
|
|
+ )
|
|
|
|
|
+ history = [m.to_llm_dict() for m in main_path_messages]
|
|
|
|
|
+ head_seq = side_branch_ctx.start_head_seq
|
|
|
|
|
|
|
|
- # 清除侧分支状态
|
|
|
|
|
|
|
+ # 清理
|
|
|
trace.context.pop("active_side_branch", None)
|
|
trace.context.pop("active_side_branch", None)
|
|
|
|
|
+ if not config.force_side_branch or len(config.force_side_branch) == 0:
|
|
|
|
|
+ config.force_side_branch = None
|
|
|
|
|
+ logger.info("反思完成,队列为空")
|
|
|
if self.trace_store:
|
|
if self.trace_store:
|
|
|
await self.trace_store.update_trace(
|
|
await self.trace_store.update_trace(
|
|
|
- trace_id,
|
|
|
|
|
- context=trace.context,
|
|
|
|
|
- head_sequence=head_seq,
|
|
|
|
|
|
|
+ trace_id, context=trace.context, head_sequence=head_seq,
|
|
|
)
|
|
)
|
|
|
-
|
|
|
|
|
- # 注意:不在这里清除 force_side_branch,因为反思侧分支可能已经设置了下一个侧分支
|
|
|
|
|
- # force_side_branch 的清除由各个分支类型自己处理
|
|
|
|
|
-
|
|
|
|
|
side_branch_ctx = None
|
|
side_branch_ctx = None
|
|
|
continue
|
|
continue
|
|
|
|
|
|
|
@@ -1518,14 +1463,50 @@ class AgentRunner:
|
|
|
"content": tool_content_for_llm,
|
|
"content": tool_content_for_llm,
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+ # on_complete 模式:goal(done=...) 后立即压缩该 goal 的消息
|
|
|
|
|
+ if (
|
|
|
|
|
+ not side_branch_ctx
|
|
|
|
|
+ and config.goal_compression == "on_complete"
|
|
|
|
|
+ and self.trace_store
|
|
|
|
|
+ and goal_tree
|
|
|
|
|
+ ):
|
|
|
|
|
+ has_goal_done = False
|
|
|
|
|
+ for tc in tool_calls:
|
|
|
|
|
+ if tc["function"]["name"] != "goal":
|
|
|
|
|
+ continue
|
|
|
|
|
+ try:
|
|
|
|
|
+ raw = tc["function"]["arguments"]
|
|
|
|
|
+ args = json.loads(raw) if isinstance(raw, str) and raw.strip() else {}
|
|
|
|
|
+ except (json.JSONDecodeError, TypeError):
|
|
|
|
|
+ args = {}
|
|
|
|
|
+ if args.get("done") is not None:
|
|
|
|
|
+ has_goal_done = True
|
|
|
|
|
+ break
|
|
|
|
|
+
|
|
|
|
|
+ if has_goal_done:
|
|
|
|
|
+ main_path_msgs = await self.trace_store.get_main_path_messages(
|
|
|
|
|
+ trace_id, head_seq
|
|
|
|
|
+ )
|
|
|
|
|
+ compressed_msgs = compress_completed_goals(main_path_msgs, goal_tree)
|
|
|
|
|
+ if len(compressed_msgs) < len(main_path_msgs):
|
|
|
|
|
+ logger.info(
|
|
|
|
|
+ "on_complete 压缩: %d -> %d 条消息",
|
|
|
|
|
+ len(main_path_msgs), len(compressed_msgs),
|
|
|
|
|
+ )
|
|
|
|
|
+ history = [msg.to_llm_dict() for msg in compressed_msgs]
|
|
|
|
|
+
|
|
|
continue # 继续循环
|
|
continue # 继续循环
|
|
|
|
|
|
|
|
- # 无工具调用,任务完成
|
|
|
|
|
- break
|
|
|
|
|
|
|
+ # 无工具调用
|
|
|
|
|
+ # 如果在侧分支中,已经在上面处理过了(不会走到这里)
|
|
|
|
|
+ # 主路径无工具调用 → 任务完成,检查是否需要完成后反思
|
|
|
|
|
+ if not side_branch_ctx and config.knowledge.enable_completion_extraction and not break_after_side_branch:
|
|
|
|
|
+ config.force_side_branch = ["reflection"]
|
|
|
|
|
+ break_after_side_branch = True
|
|
|
|
|
+ logger.info("任务完成,进入完成后反思侧分支")
|
|
|
|
|
+ continue
|
|
|
|
|
|
|
|
- # 任务完成后复盘提取
|
|
|
|
|
- if config.knowledge.enable_completion_extraction:
|
|
|
|
|
- await self._extract_knowledge_on_completion(trace_id, history, config)
|
|
|
|
|
|
|
+ break
|
|
|
|
|
|
|
|
# 清理 trace 相关的跟踪数据
|
|
# 清理 trace 相关的跟踪数据
|
|
|
self._context_warned.pop(trace_id, None)
|
|
self._context_warned.pop(trace_id, None)
|
|
@@ -1594,95 +1575,6 @@ class AgentRunner:
|
|
|
|
|
|
|
|
return new_history
|
|
return new_history
|
|
|
|
|
|
|
|
- async def _run_reflect(
|
|
|
|
|
- self,
|
|
|
|
|
- trace_id: str,
|
|
|
|
|
- history: List[Dict],
|
|
|
|
|
- config: RunConfig,
|
|
|
|
|
- reflect_prompt: str,
|
|
|
|
|
- source_name: str,
|
|
|
|
|
- ) -> None:
|
|
|
|
|
- """
|
|
|
|
|
- 执行反思提取:LLM 对历史消息进行反思,直接调用 knowledge_save 工具保存经验。
|
|
|
|
|
-
|
|
|
|
|
- Args:
|
|
|
|
|
- trace_id: Trace ID(作为知识的 message_id)
|
|
|
|
|
- history: 当前对话历史
|
|
|
|
|
- config: 运行配置
|
|
|
|
|
- reflect_prompt: 反思 prompt
|
|
|
|
|
- source_name: 来源名称(用于区分压缩时/完成时)
|
|
|
|
|
- """
|
|
|
|
|
- try:
|
|
|
|
|
- reflect_messages = list(history) + [{"role": "user", "content": reflect_prompt}]
|
|
|
|
|
- reflect_messages = self._add_cache_control(
|
|
|
|
|
- reflect_messages, config.model, config.enable_prompt_caching
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- # 只暴露 knowledge_save 工具,让 LLM 直接调用
|
|
|
|
|
- knowledge_save_schema = self._get_tool_schemas(["knowledge_save"])
|
|
|
|
|
-
|
|
|
|
|
- reflect_result = await self.llm_call(
|
|
|
|
|
- messages=reflect_messages,
|
|
|
|
|
- model=config.model,
|
|
|
|
|
- tools=knowledge_save_schema,
|
|
|
|
|
- temperature=0.2,
|
|
|
|
|
- **config.extra_llm_params,
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- tool_calls = reflect_result.get("tool_calls") or []
|
|
|
|
|
- if not tool_calls:
|
|
|
|
|
- logger.info("反思阶段无经验保存 (source=%s)", source_name)
|
|
|
|
|
- return
|
|
|
|
|
-
|
|
|
|
|
- saved_count = 0
|
|
|
|
|
- for tc in tool_calls:
|
|
|
|
|
- tool_name = tc.get("function", {}).get("name")
|
|
|
|
|
- if tool_name != "knowledge_save":
|
|
|
|
|
- continue
|
|
|
|
|
-
|
|
|
|
|
- tool_args = tc.get("function", {}).get("arguments") or {}
|
|
|
|
|
- if isinstance(tool_args, str):
|
|
|
|
|
- tool_args = json.loads(tool_args) if tool_args.strip() else {}
|
|
|
|
|
-
|
|
|
|
|
- # 注入来源信息(LLM 不需要填写这些字段)
|
|
|
|
|
- tool_args.setdefault("source_name", source_name)
|
|
|
|
|
- tool_args.setdefault("source_category", "exp")
|
|
|
|
|
- tool_args.setdefault("message_id", trace_id)
|
|
|
|
|
-
|
|
|
|
|
- try:
|
|
|
|
|
- await self.tools.execute(
|
|
|
|
|
- "knowledge_save",
|
|
|
|
|
- tool_args,
|
|
|
|
|
- uid=config.uid or "",
|
|
|
|
|
- context={
|
|
|
|
|
- "store": self.trace_store,
|
|
|
|
|
- "trace_id": trace_id,
|
|
|
|
|
- "knowledge_config": config.knowledge,
|
|
|
|
|
- },
|
|
|
|
|
- )
|
|
|
|
|
- saved_count += 1
|
|
|
|
|
- except Exception as e:
|
|
|
|
|
- logger.warning("保存经验失败: %s", e)
|
|
|
|
|
-
|
|
|
|
|
- logger.info("已提取并保存 %d 条经验 (source=%s)", saved_count, source_name)
|
|
|
|
|
-
|
|
|
|
|
- except Exception as e:
|
|
|
|
|
- logger.error("知识反思提取失败 (source=%s): %s", source_name, e)
|
|
|
|
|
-
|
|
|
|
|
- async def _extract_knowledge_on_completion(
|
|
|
|
|
- self,
|
|
|
|
|
- trace_id: str,
|
|
|
|
|
- history: List[Dict],
|
|
|
|
|
- config: RunConfig,
|
|
|
|
|
- ) -> None:
|
|
|
|
|
- """任务完成后执行全局复盘,提取经验保存到知识库。"""
|
|
|
|
|
- logger.info("任务完成后复盘提取: trace=%s", trace_id)
|
|
|
|
|
- await self._run_reflect(
|
|
|
|
|
- trace_id, history, config,
|
|
|
|
|
- reflect_prompt=config.knowledge.get_completion_reflect_prompt(),
|
|
|
|
|
- source_name="completion_reflection",
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
# ===== 回溯(Rewind)=====
|
|
# ===== 回溯(Rewind)=====
|
|
|
|
|
|
|
|
async def _rewind(
|
|
async def _rewind(
|