Talegorithm 2 часов назад
Родитель
Сommit
e4c1f9fbfe
3 измененных файлов с 185 добавлено и 10 удалено
  1. 157 4
      agent/cli/interactive.py
  2. 14 3
      examples/plan/run.py
  3. 14 3
      examples/research/run.py

+ 157 - 4
agent/cli/interactive.py

@@ -141,12 +141,13 @@ class InteractiveController:
         print("  2. 触发经验总结(reflect)")
         print("  3. 查看当前 GoalTree")
         print("  4. 手动压缩上下文(compact)")
-        print("  5. 继续执行")
-        print("  6. 停止执行")
+        print("  5. 从指定消息续跑")
+        print("  6. 继续执行")
+        print("  7. 停止执行")
         print("=" * 60)
 
         while True:
-            choice = input("请输入选项 (1-6): ").strip()
+            choice = input("请输入选项 (1-7): ").strip()
 
             if choice == "1":
                 # 插入干预消息
@@ -189,11 +190,16 @@ class InteractiveController:
                 continue
 
             elif choice == "5":
+                # 从指定消息续跑
+                await self.resume_from_message(trace_id)
+                return {"action": "stop"}  # 返回 stop,让外层循环退出
+
+            elif choice == "6":
                 # 继续执行
                 print("\n继续执行...")
                 return {"action": "continue"}
 
-            elif choice == "6":
+            elif choice == "7":
                 # 停止执行
                 print("\n停止执行...")
                 return {"action": "stop"}
@@ -272,3 +278,150 @@ class InteractiveController:
             print(f"❌ 压缩任务启动失败: {e}")
         except Exception as e:
             print(f"❌ 发生错误: {e}")
+
+    async def resume_from_message(self, trace_id: str):
+        """
+        从指定消息续跑
+
+        让用户选择一条消息,然后从该消息之后重新执行。
+
+        Args:
+            trace_id: Trace ID
+        """
+        print("\n正在加载消息列表...")
+
+        # 1. 获取所有消息
+        messages = await self.store.get_messages(trace_id)
+        if not messages:
+            print("❌ 没有找到任何消息")
+            return
+
+        # 2. 显示消息列表(只显示 user 和 assistant 消息)
+        display_messages = [
+            msg for msg in messages
+            if msg.role in ("user", "assistant")
+        ]
+
+        if not display_messages:
+            print("❌ 没有可选择的消息")
+            return
+
+        print("\n" + "=" * 60)
+        print("  消息列表")
+        print("=" * 60)
+
+        for i, msg in enumerate(display_messages, 1):
+            role_label = "👤 User" if msg.role == "user" else "🤖 Assistant"
+            content_preview = self._get_content_preview(msg.content)
+            print(f"{i}. [{msg.sequence:04d}] {role_label}: {content_preview}")
+
+        print("=" * 60)
+
+        # 3. 让用户选择
+        while True:
+            choice = input(f"\n请选择消息编号 (1-{len(display_messages)}),或输入 'c' 取消: ").strip()
+
+            if choice.lower() == 'c':
+                print("已取消")
+                return
+
+            try:
+                idx = int(choice) - 1
+                if 0 <= idx < len(display_messages):
+                    selected_msg = display_messages[idx]
+                    break
+                else:
+                    print(f"无效编号,请输入 1-{len(display_messages)}")
+            except ValueError:
+                print("无效输入,请输入数字或 'c'")
+
+        # 4. 确认是否重新生成最后一条消息
+        regenerate_last = False
+        if selected_msg.role == "assistant":
+            confirm = input("\n是否重新生成这条 Assistant 消息?(y/n): ").strip().lower()
+            regenerate_last = (confirm == 'y')
+
+        # 5. 调用 runner.run() 续跑
+        print(f"\n从消息 {selected_msg.sequence:04d} 之后续跑...")
+        if regenerate_last:
+            print("将重新生成最后一条 Assistant 消息")
+
+        try:
+            # 加载 trace 和消息历史
+            trace = await self.store.get_trace(trace_id)
+            if not trace:
+                print("❌ Trace 不存在")
+                return
+
+            # 截断消息到指定位置
+            truncated_messages = []
+            for msg in messages:
+                if msg.sequence <= selected_msg.sequence:
+                    truncated_messages.append({
+                        "role": msg.role,
+                        "content": msg.content,
+                        "id": msg.message_id,
+                    })
+
+            # 如果需要重新生成,删除最后一条 assistant 消息
+            if regenerate_last and truncated_messages and truncated_messages[-1]["role"] == "assistant":
+                truncated_messages.pop()
+
+            # 调用 runner.run() 续跑
+            print("\n开始执行...")
+            async for event in self.runner.run(
+                messages=truncated_messages,
+                trace_id=trace_id,
+                model=trace.model,
+                temperature=trace.llm_params.get("temperature", 0.3),
+                max_iterations=200,
+                tools=None,  # 使用原有配置
+            ):
+                # 简单输出事件
+                if event.get("type") == "message":
+                    msg = event.get("message")
+                    if msg and msg.get("role") == "assistant":
+                        content = msg.get("content", {})
+                        if isinstance(content, dict):
+                            text = content.get("text", "")
+                        else:
+                            text = str(content)
+                        if text:
+                            print(f"\n🤖 Assistant: {text[:200]}...")
+
+            print("\n✅ 执行完成")
+
+        except Exception as e:
+            print(f"❌ 执行失败: {e}")
+            import traceback
+            traceback.print_exc()
+
+    def _get_content_preview(self, content: Any, max_length: int = 60) -> str:
+        """
+        获取消息内容预览
+
+        Args:
+            content: 消息内容
+            max_length: 最大长度
+
+        Returns:
+            内容预览字符串
+        """
+        if isinstance(content, dict):
+            text = content.get("text", "")
+            tool_calls = content.get("tool_calls", [])
+            if text:
+                preview = text.strip()
+            elif tool_calls:
+                preview = f"[调用工具: {', '.join(tc.get('function', {}).get('name', '?') for tc in tool_calls)}]"
+            else:
+                preview = "[空消息]"
+        elif isinstance(content, str):
+            preview = content.strip()
+        else:
+            preview = str(content)
+
+        if len(preview) > max_length:
+            preview = preview[:max_length] + "..."
+
+        return preview

+ 14 - 3
examples/plan/run.py

@@ -140,6 +140,7 @@ async def main():
         print(f"恢复已有 Trace: {resume_trace_id[:8]}...")
         print(f"   - 状态: {existing_trace.status}")
         print(f"   - 消息数: {existing_trace.total_messages}")
+        print(f"\n💡 提示:恢复 Trace 时会先进入交互菜单,您可以选择从指定消息续跑")
     else:
         print(f"启动新 Agent...")
 
@@ -166,16 +167,24 @@ async def main():
 
             final_response = ""
 
-            # 如果 trace 已完成/失败且没有新消息,进入交互菜单
+            # 如果是恢复 trace 或 trace 已完成/失败且没有新消息,进入交互菜单
             if current_trace_id and initial_messages is None:
                 check_trace = await store.get_trace(current_trace_id)
-                if check_trace and check_trace.status in ("completed", "failed"):
+                if check_trace:
+                    # 显示 trace 状态
                     if check_trace.status == "completed":
                         print(f"\n[Trace] ✅ 已完成")
                         print(f"  - Total messages: {check_trace.total_messages}")
                         print(f"  - Total cost: ${check_trace.total_cost:.4f}")
-                    else:
+                    elif check_trace.status == "failed":
                         print(f"\n[Trace] ❌ 已失败: {check_trace.error_message}")
+                    elif check_trace.status == "stopped":
+                        print(f"\n[Trace] ⏸️ 已停止")
+                        print(f"  - Total messages: {check_trace.total_messages}")
+                    else:
+                        print(f"\n[Trace] 📊 状态: {check_trace.status}")
+                        print(f"  - Total messages: {check_trace.total_messages}")
+
                     current_sequence = check_trace.head_sequence
 
                     menu_result = await interactive.show_menu(current_trace_id, current_sequence)
@@ -193,6 +202,8 @@ async def main():
                         continue
                     break
 
+            # 如果没有进入菜单(新建 trace),设置初始消息
+            if initial_messages is None:
                 initial_messages = []
 
             print(f"{'▶️ 开始执行...' if not current_trace_id else '▶️ 继续执行...'}")

+ 14 - 3
examples/research/run.py

@@ -140,6 +140,7 @@ async def main():
         print(f"恢复已有 Trace: {resume_trace_id[:8]}...")
         print(f"   - 状态: {existing_trace.status}")
         print(f"   - 消息数: {existing_trace.total_messages}")
+        print(f"\n💡 提示:恢复 Trace 时会先进入交互菜单,您可以选择从指定消息续跑")
     else:
         print(f"启动新 Agent...")
 
@@ -166,16 +167,24 @@ async def main():
 
             final_response = ""
 
-            # 如果 trace 已完成/失败且没有新消息,进入交互菜单
+            # 如果是恢复 trace 或 trace 已完成/失败且没有新消息,进入交互菜单
             if current_trace_id and initial_messages is None:
                 check_trace = await store.get_trace(current_trace_id)
-                if check_trace and check_trace.status in ("completed", "failed"):
+                if check_trace:
+                    # 显示 trace 状态
                     if check_trace.status == "completed":
                         print(f"\n[Trace] ✅ 已完成")
                         print(f"  - Total messages: {check_trace.total_messages}")
                         print(f"  - Total cost: ${check_trace.total_cost:.4f}")
-                    else:
+                    elif check_trace.status == "failed":
                         print(f"\n[Trace] ❌ 已失败: {check_trace.error_message}")
+                    elif check_trace.status == "stopped":
+                        print(f"\n[Trace] ⏸️ 已停止")
+                        print(f"  - Total messages: {check_trace.total_messages}")
+                    else:
+                        print(f"\n[Trace] 📊 状态: {check_trace.status}")
+                        print(f"  - Total messages: {check_trace.total_messages}")
+
                     current_sequence = check_trace.head_sequence
 
                     menu_result = await interactive.show_menu(current_trace_id, current_sequence)
@@ -193,6 +202,8 @@ async def main():
                         continue
                     break
 
+            # 如果没有进入菜单(新建 trace),设置初始消息
+            if initial_messages is None:
                 initial_messages = []
 
             print(f"{'▶️ 开始执行...' if not current_trace_id else '▶️ 继续执行...'}")