Преглед на файлове

Merge branch 'main' of https://git.yishihui.com/howard/Agent

guantao преди 6 часа
родител
ревизия
63b3e42ecf
променени са 4 файла, в които са добавени 134 реда и са изтрити 42 реда
  1. 68 40
      agent/core/runner.py
  2. 3 0
      agent/tools/builtin/__init__.py
  3. 61 0
      agent/tools/builtin/context.py
  4. 2 2
      agent/trace/run_api.py

+ 68 - 40
agent/core/runner.py

@@ -106,8 +106,9 @@ class RunConfig:
     tools: Optional[List[str]] = None          # None = 全部已注册工具
     tools: Optional[List[str]] = None          # None = 全部已注册工具
     side_branch_max_turns: int = 5             # 侧分支最大轮次(压缩/反思)
     side_branch_max_turns: int = 5             # 侧分支最大轮次(压缩/反思)
 
 
-    # --- 强制侧分支(用于 API 手动触发)---
-    force_side_branch: Optional[Literal["compression", "reflection"]] = None
+    # --- 强制侧分支(用于 API 手动触发或自动压缩流程)---
+    # 使用列表作为侧分支队列,每次完成一个侧分支后 pop(0) 取下一个
+    force_side_branch: Optional[List[Literal["compression", "reflection"]]] = None
 
 
     # --- 框架层参数 ---
     # --- 框架层参数 ---
     agent_type: str = "default"
     agent_type: str = "default"
@@ -157,6 +158,7 @@ BUILTIN_TOOLS = [
     "goal",
     "goal",
     "agent",
     "agent",
     "evaluate",
     "evaluate",
+    "get_current_context",
 
 
     # 搜索工具
     # 搜索工具
     "search_posts",
     "search_posts",
@@ -780,8 +782,9 @@ class AgentRunner:
             return history, head_seq, sequence, False
             return history, head_seq, sequence, False
 
 
         # 知识提取:在任何压缩发生前,用完整 history 做反思(进入反思侧分支)
         # 知识提取:在任何压缩发生前,用完整 history 做反思(进入反思侧分支)
-        if config.knowledge.enable_extraction:
-            # 返回标志,让主循环进入反思侧分支
+        if config.knowledge.enable_extraction and not config.force_side_branch:
+            # 设置侧分支队列:先反思,再压缩
+            config.force_side_branch = ["reflection", "compression"]
             return history, head_seq, sequence, True
             return history, head_seq, sequence, True
 
 
         # Level 1 压缩:GoalTree 过滤
         # Level 1 压缩:GoalTree 过滤
@@ -823,7 +826,10 @@ class AgentRunner:
                 "Level 1 后仍超阈值 (消息数=%d/%d, token=%d/%d),需要进入压缩侧分支",
                 "Level 1 后仍超阈值 (消息数=%d/%d, token=%d/%d),需要进入压缩侧分支",
                 msg_count_after, compression_config.max_messages, token_count_after, max_tokens,
                 msg_count_after, compression_config.max_messages, token_count_after, max_tokens,
             )
             )
-            # 返回标志,让主循环进入压缩侧分支
+            # 如果还没有设置侧分支(说明没有启用知识提取),直接进入压缩
+            if not config.force_side_branch:
+                config.force_side_branch = ["compression"]
+            # 返回标志,让主循环进入侧分支
             return history, head_seq, sequence, True
             return history, head_seq, sequence, True
 
 
         # 压缩完成后,输出最终发给模型的消息列表
         # 压缩完成后,输出最终发给模型的消息列表
@@ -887,13 +893,14 @@ class AgentRunner:
             return history, start_head_seq, sequence
             return history, start_head_seq, sequence
 
 
         # 创建 summary 消息
         # 创建 summary 消息
+        from agent.core.prompts import build_summary_header
         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=start_head_seq,
             parent_sequence=start_head_seq,
             branch_type=None,  # 主路径
             branch_type=None,  # 主路径
-            content=f"[压缩总结 - Fallback]\n{summary_text}",
+            content=build_summary_header(summary_text),
         )
         )
 
 
         if self.trace_store:
         if self.trace_store:
@@ -1009,12 +1016,12 @@ class AgentRunner:
 
 
             # 进入侧分支
             # 进入侧分支
             if needs_enter_side_branch and not side_branch_ctx:
             if needs_enter_side_branch and not side_branch_ctx:
-                # 判断侧分支类型
-                if config.force_side_branch:
-                    # API 强制触发
-                    branch_type = config.force_side_branch
+                # 从队列中取出第一个侧分支类型
+                if config.force_side_branch and isinstance(config.force_side_branch, list) and len(config.force_side_branch) > 0:
+                    branch_type = config.force_side_branch.pop(0)
+                    logger.info(f"从队列取出侧分支: {branch_type}, 剩余队列: {config.force_side_branch}")
                 elif config.knowledge.enable_extraction:
                 elif config.knowledge.enable_extraction:
-                    # 自动触发:反思
+                    # 兼容旧的单值模式(如果 force_side_branch 是字符串)
                     branch_type = "reflection"
                     branch_type = "reflection"
                 else:
                 else:
                     # 自动触发:压缩
                     # 自动触发:压缩
@@ -1086,31 +1093,6 @@ class AgentRunner:
                 config.enable_prompt_caching
                 config.enable_prompt_caching
             )
             )
 
 
-            # 周期性注入 GoalTree + Collaborators(动态内容追加在缓存点之后)
-            # 仅在主路径执行
-            if not side_branch_ctx and iteration % CONTEXT_INJECTION_INTERVAL == 0:
-                context_injection = self._build_context_injection(trace, goal_tree)
-                if context_injection:
-                    system_msg = {"role": "system", "content": context_injection}
-                    llm_messages.append(system_msg)
-
-                    # 持久化上下文注入消息
-                    if self.trace_store:
-                        current_goal_id = goal_tree.current_id if (goal_tree and goal_tree.current_id) else None
-                        system_message = Message.create(
-                            trace_id=trace_id,
-                            role="system",
-                            sequence=sequence,
-                            goal_id=current_goal_id,
-                            parent_sequence=head_seq if head_seq > 0 else None,
-                            content=f"[上下文注入]\n{context_injection}",
-                        )
-                        await self.trace_store.add_message(system_message)
-                        history.append(system_msg)
-                        head_seq = sequence
-                        sequence += 1
-
-
             # 调用 LLM(等待完成后再检查 cancel 信号,不中断正在进行的调用)
             # 调用 LLM(等待完成后再检查 cancel 信号,不中断正在进行的调用)
             result = await self.llm_call(
             result = await self.llm_call(
                 messages=llm_messages,
                 messages=llm_messages,
@@ -1129,6 +1111,30 @@ class AgentRunner:
             cache_creation_tokens = result.get("cache_creation_tokens")
             cache_creation_tokens = result.get("cache_creation_tokens")
             cache_read_tokens = result.get("cache_read_tokens")
             cache_read_tokens = result.get("cache_read_tokens")
 
 
+            # 周期性自动注入上下文(仅主路径)
+            if not side_branch_ctx and iteration % CONTEXT_INJECTION_INTERVAL == 0:
+                # 检查是否已经调用了 get_current_context
+                if tool_calls:
+                    has_context_call = any(
+                        tc.get("function", {}).get("name") == "get_current_context"
+                        for tc in tool_calls
+                    )
+                else:
+                    has_context_call = False
+                    tool_calls = []
+
+                if not has_context_call:
+                    # 手动添加 get_current_context 工具调用
+                    import uuid
+                    context_call_id = f"call_context_{uuid.uuid4().hex[:8]}"
+                    tool_calls.append({
+                        "id": context_call_id,
+                        "type": "function",
+                        "function": {"name": "get_current_context", "arguments": "{}"}
+                    })
+                    logger.info(f"[周期性注入] 自动添加 get_current_context 工具调用 (iteration={iteration})")
+
+
             # 按需自动创建 root goal(仅主路径)
             # 按需自动创建 root goal(仅主路径)
             if not side_branch_ctx and goal_tree and not goal_tree.goals and tool_calls:
             if not side_branch_ctx and goal_tree and not goal_tree.goals and tool_calls:
                 has_goal_call = any(
                 has_goal_call = any(
@@ -1239,6 +1245,12 @@ class AgentRunner:
 
 
                         # 清除侧分支状态
                         # 清除侧分支状态
                         trace.context.pop("active_side_branch", None)
                         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:
                         if self.trace_store:
                             await self.trace_store.update_trace(
                             await self.trace_store.update_trace(
                                 trace_id, context=trace.context
                                 trace_id, context=trace.context
@@ -1286,14 +1298,22 @@ class AgentRunner:
                             logger.warning("侧分支未生成有效 summary,使用默认")
                             logger.warning("侧分支未生成有效 summary,使用默认")
                             summary_text = "压缩完成"
                             summary_text = "压缩完成"
 
 
-                        # 创建主路径的 summary 消息
+                        # 创建主路径的 summary 消息(末尾追加详细 GoalTree)
+                        from agent.core.prompts import build_summary_header
+                        summary_content = build_summary_header(summary_text)
+
+                        # 追加详细 GoalTree(压缩后立即注入)
+                        if goal_tree and goal_tree.goals:
+                            goal_tree_detail = goal_tree.to_prompt(include_summary=True)
+                            summary_content += f"\n\n## Current Plan\n\n{goal_tree_detail}"
+
                         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,
                             parent_sequence=side_branch_ctx.start_head_seq,
                             branch_type=None,  # 回到主路径
                             branch_type=None,  # 回到主路径
-                            content=f"[压缩总结]\n{summary_text}",
+                            content=summary_content,
                         )
                         )
 
 
                         if self.trace_store:
                         if self.trace_store:
@@ -1312,6 +1332,9 @@ class AgentRunner:
 
 
                         logger.info(f"压缩侧分支完成,history 长度: {len(history)}")
                         logger.info(f"压缩侧分支完成,history 长度: {len(history)}")
 
 
+                        # 清除侧分支队列
+                        config.force_side_branch = None
+
                     elif side_branch_ctx.type == "reflection":
                     elif side_branch_ctx.type == "reflection":
                         # 反思侧分支:直接恢复主路径
                         # 反思侧分支:直接恢复主路径
                         logger.info("反思侧分支完成")
                         logger.info("反思侧分支完成")
@@ -1323,6 +1346,11 @@ class AgentRunner:
                             history = [m.to_llm_dict() for m in main_path_messages]
                             history = [m.to_llm_dict() for m in main_path_messages]
                             head_seq = side_branch_ctx.start_head_seq
                             head_seq = side_branch_ctx.start_head_seq
 
 
+                        # 队列中如果还有侧分支,保持 force_side_branch;否则清空
+                        if not config.force_side_branch or len(config.force_side_branch) == 0:
+                            config.force_side_branch = None
+                            logger.info("反思完成,队列为空")
+
                     # 清除侧分支状态
                     # 清除侧分支状态
                     trace.context.pop("active_side_branch", None)
                     trace.context.pop("active_side_branch", None)
                     if self.trace_store:
                     if self.trace_store:
@@ -1332,8 +1360,8 @@ class AgentRunner:
                             head_sequence=head_seq,
                             head_sequence=head_seq,
                         )
                         )
 
 
-                    # 清除强制侧分支配置(避免影响后续续跑)
-                    config.force_side_branch = None
+                    # 注意:不在这里清除 force_side_branch,因为反思侧分支可能已经设置了下一个侧分支
+                    # force_side_branch 的清除由各个分支类型自己处理
 
 
                     side_branch_ctx = None
                     side_branch_ctx = None
                     continue
                     continue

+ 3 - 0
agent/tools/builtin/__init__.py

@@ -19,6 +19,7 @@ from agent.tools.builtin.search import search_posts, get_search_suggestions
 from agent.tools.builtin.sandbox import (sandbox_create_environment, sandbox_run_shell,
 from agent.tools.builtin.sandbox import (sandbox_create_environment, sandbox_run_shell,
                                          sandbox_rebuild_with_ports,sandbox_destroy_environment)
                                          sandbox_rebuild_with_ports,sandbox_destroy_environment)
 from agent.tools.builtin.knowledge import(knowledge_search,knowledge_save,knowledge_list,knowledge_update,knowledge_batch_update,knowledge_slim)
 from agent.tools.builtin.knowledge import(knowledge_search,knowledge_save,knowledge_list,knowledge_update,knowledge_batch_update,knowledge_slim)
+from agent.tools.builtin.context import get_current_context
 from agent.trace.goal_tool import goal
 from agent.trace.goal_tool import goal
 # 导入浏览器工具以触发注册
 # 导入浏览器工具以触发注册
 import agent.tools.builtin.browser  # noqa: F401
 import agent.tools.builtin.browser  # noqa: F401
@@ -50,6 +51,8 @@ __all__ = [
     "sandbox_run_shell",
     "sandbox_run_shell",
     "sandbox_rebuild_with_ports",
     "sandbox_rebuild_with_ports",
     "sandbox_destroy_environment",
     "sandbox_destroy_environment",
+    # 上下文工具
+    "get_current_context",
     # Goal 管理
     # Goal 管理
     "goal",
     "goal",
 ]
 ]

+ 61 - 0
agent/tools/builtin/context.py

@@ -0,0 +1,61 @@
+"""
+上下文工具 - 获取当前执行上下文
+
+提供 get_current_context 工具,让 Agent 可以主动获取:
+- 当前计划(GoalTree)
+- 焦点提醒
+- 协作者状态
+
+框架也会在特定轮次自动调用此工具进行周期性上下文刷新。
+"""
+
+from agent.tools import tool, ToolResult, ToolContext
+
+
+@tool(
+    description="获取当前执行上下文,包括计划状态、焦点提醒、协作者信息等。当你感到困惑或需要回顾当前任务状态时调用。",
+    hidden_params=["context"]
+)
+async def get_current_context(
+    context: ToolContext,
+) -> ToolResult:
+    """
+    获取当前执行上下文
+
+    Returns:
+        ToolResult: 包含 GoalTree、焦点提醒、协作者状态等信息
+    """
+    runner = context.get("runner")
+    goal_tree = context.get("goal_tree")
+    trace_id = context.get("trace_id")
+
+    if not runner:
+        return ToolResult(
+            title="❌ 无法获取上下文",
+            output="Runner 未初始化",
+            error="Runner not available"
+        )
+
+    # 获取 trace 对象
+    trace = None
+    if runner.trace_store and trace_id:
+        trace = await runner.trace_store.get_trace(trace_id)
+
+    # 构建上下文内容(复用 runner 的 _build_context_injection 方法)
+    if hasattr(runner, '_build_context_injection'):
+        context_content = runner._build_context_injection(trace, goal_tree)
+    else:
+        # Fallback:只返回 GoalTree
+        if goal_tree and goal_tree.goals:
+            context_content = f"## Current Plan\n\n{goal_tree.to_prompt()}"
+        else:
+            context_content = "暂无计划信息"
+
+    if not context_content:
+        context_content = "当前无需要刷新的上下文信息"
+
+    return ToolResult(
+        title="📋 当前执行上下文",
+        output=context_content,
+        long_term_memory="已刷新执行上下文",
+    )

+ 2 - 2
agent/trace/run_api.py

@@ -487,7 +487,7 @@ async def reflect_trace(trace_id: str, req: ReflectRequest):
     config = RunConfig(
     config = RunConfig(
         trace_id=trace_id,
         trace_id=trace_id,
         model=trace.model or "gpt-4o",
         model=trace.model or "gpt-4o",
-        force_side_branch="reflection",
+        force_side_branch=["reflection"],  # 使用列表格式
         max_iterations=20,  # 给侧分支足够的轮次
         max_iterations=20,  # 给侧分支足够的轮次
         enable_prompt_caching=True,
         enable_prompt_caching=True,
     )
     )
@@ -535,7 +535,7 @@ async def compact_trace(trace_id: str):
     config = RunConfig(
     config = RunConfig(
         trace_id=trace_id,
         trace_id=trace_id,
         model=trace.model or "gpt-4o",
         model=trace.model or "gpt-4o",
-        force_side_branch="compression",
+        force_side_branch=["compression"],  # 使用列表格式
         max_iterations=20,  # 给侧分支足够的轮次
         max_iterations=20,  # 给侧分支足够的轮次
         enable_prompt_caching=True,
         enable_prompt_caching=True,
     )
     )