Browse Source

Merge remote-tracking branch 'refs/remotes/origin/main'

Talegorithm 3 weeks ago
parent
commit
028fa57f18

+ 3 - 0
agent/core/runner.py

@@ -583,6 +583,9 @@ class AgentRunner:
                     })
 
                     for tc in tool_calls:
+                        # 每次工具执行前重新获取最新的 goal_id(处理并行 tool_calls 的情况)
+                        current_goal_id = goal_tree.current_id if (goal_tree and goal_tree.current_id) else None
+
                         tool_name = tc["function"]["name"]
                         tool_args = tc["function"]["arguments"]
 

+ 16 - 7
agent/tools/builtin/subagent.py

@@ -247,7 +247,10 @@ async def _handle_explore_mode(
     for i, branch in enumerate(branches):
         # 生成唯一的 sub_trace_id
         sub_trace_id = generate_sub_trace_id(current_trace_id, f"explore-{i+1:03d}")
-        sub_trace_ids.append(sub_trace_id)
+        sub_trace_ids.append({
+            "trace_id": sub_trace_id,
+            "mission": branch
+        })
 
         # 创建 Sub-Trace
         parent_trace = await store.get_trace(current_trace_id)
@@ -301,13 +304,13 @@ async def _handle_explore_mode(
             }
             processed_results.append(error_result)
             await broadcast_sub_trace_completed(
-                current_trace_id, sub_trace_ids[i],
+                current_trace_id, sub_trace_ids[i]["trace_id"],
                 "failed", str(result), {}
             )
         else:
             processed_results.append(result)
             await broadcast_sub_trace_completed(
-                current_trace_id, sub_trace_ids[i],
+                current_trace_id, sub_trace_ids[i]["trace_id"],
                 result.get("status", "completed"),
                 result.get("summary", ""),
                 result.get("stats", {})
@@ -350,7 +353,10 @@ async def _handle_delegate_mode(
         if not existing_trace:
             return {"status": "failed", "error": f"Continue-from trace not found: {continue_from}"}
         sub_trace_id = continue_from
-        sub_trace_ids = [sub_trace_id]
+        # 获取 mission
+        goal_tree = await store.get_goal_tree(continue_from)
+        mission = goal_tree.mission if goal_tree else task
+        sub_trace_ids = [{"trace_id": sub_trace_id, "mission": mission}]
     else:
         parent_trace = await store.get_trace(current_trace_id)
         sub_trace_id = generate_sub_trace_id(current_trace_id, "delegate")
@@ -369,7 +375,7 @@ async def _handle_delegate_mode(
         )
         await store.create_trace(sub_trace)
         await store.update_goal_tree(sub_trace_id, GoalTree(mission=task))
-        sub_trace_ids = [sub_trace_id]
+        sub_trace_ids = [{"trace_id": sub_trace_id, "mission": task}]
 
         # 广播 sub_trace_started
         await broadcast_sub_trace_started(
@@ -458,7 +464,10 @@ async def _handle_evaluate_mode(
         if not existing_trace:
             return {"status": "failed", "error": f"Continue-from trace not found: {continue_from}"}
         sub_trace_id = continue_from
-        sub_trace_ids = [sub_trace_id]
+        # 获取 mission
+        goal_tree = await store.get_goal_tree(continue_from)
+        mission = goal_tree.mission if goal_tree else task_prompt
+        sub_trace_ids = [{"trace_id": sub_trace_id, "mission": mission}]
     else:
         parent_trace = await store.get_trace(current_trace_id)
         sub_trace_id = generate_sub_trace_id(current_trace_id, "evaluate")
@@ -477,7 +486,7 @@ async def _handle_evaluate_mode(
         )
         await store.create_trace(sub_trace)
         await store.update_goal_tree(sub_trace_id, GoalTree(mission=task_prompt))
-        sub_trace_ids = [sub_trace_id]
+        sub_trace_ids = [{"trace_id": sub_trace_id, "mission": task_prompt}]
 
         # 广播 sub_trace_started
         await broadcast_sub_trace_started(

+ 1 - 1
agent/trace/goal_models.py

@@ -62,7 +62,7 @@ class Goal:
     summary: Optional[str] = None            # 完成/放弃时的总结
 
     # agent_call 特有
-    sub_trace_ids: Optional[List[str]] = None      # 启动的 Sub-Trace IDs
+    sub_trace_ids: Optional[List[Dict[str, str]]] = None      # 启动的 Sub-Trace 信息 [{"trace_id": "...", "mission": "..."}]
     agent_call_mode: Optional[str] = None          # "explore" | "delegate" | "sequential"
     sub_trace_metadata: Optional[Dict[str, Dict[str, Any]]] = None  # Sub-Trace 元数据
 

+ 9 - 1
api_server.py

@@ -5,7 +5,9 @@ API Server - FastAPI 应用入口
 """
 
 import logging
-from fastapi import FastAPI
+import json
+import os
+from fastapi import FastAPI, Request, WebSocket
 from fastapi.middleware.cors import CORSMiddleware
 import uvicorn
 
@@ -59,6 +61,12 @@ app.include_router(api_router)
 # Step 树 WebSocket
 app.include_router(ws_router)
 
+@app.websocket("/ws_ping")
+async def ws_ping(websocket: WebSocket):
+    await websocket.accept()
+    await websocket.send_text("pong")
+    await websocket.close()
+
 
 # ===== 健康检查 =====
 

+ 17 - 5
frontend/API.md

@@ -139,7 +139,10 @@ GET /api/traces/{trace_id}
         "reason": "评估不同技术选型",
         "status": "in_progress",
         "agent_call_mode": "explore",
-        "sub_trace_ids": ["abc123.A", "abc123.B"],
+        "sub_trace_ids": [
+          {"trace_id": "abc123.A", "mission": "JWT 方案"},
+          {"trace_id": "abc123.B", "mission": "Session 方案"}
+        ],
         "sub_trace_metadata": null,
         "self_stats": { "message_count": 0, "total_tokens": 0, "total_cost": 0.0, "preview": null },
         "cumulative_stats": { "message_count": 0, "total_tokens": 0, "total_cost": 0.0, "preview": null }
@@ -208,8 +211,14 @@ GET /api/traces/{trace_id}
         "status": "completed",
         "agent_call_mode": "explore",
         "sub_trace_ids": [
-          "abc123@explore-20260204220012-001",
-          "abc123@explore-20260204220012-002"
+          {
+            "trace_id": "abc123@explore-20260204220012-001",
+            "mission": "JWT 方案"
+          },
+          {
+            "trace_id": "abc123@explore-20260204220012-002",
+            "mission": "Session 方案"
+          }
         ],
         "sub_trace_metadata": {
           "abc123@explore-20260204220012-001": {
@@ -692,7 +701,7 @@ if (data.event === 'sub_trace_completed') {
 | `reason` | string | 创建理由(为什么做)|
 | `status` | string | `pending` / `in_progress` / `completed` / `abandoned` |
 | `summary` | string \| null | 完成/放弃时的总结 |
-| `sub_trace_ids` | string[] \| null | 启动的 Sub-Trace IDs(仅 agent_call)|
+| `sub_trace_ids` | Array<{trace_id: string, mission: string}> \| null | 启动的 Sub-Trace 信息(仅 agent_call)|
 | `agent_call_mode` | string \| null | "explore" / "delegate" / "sequential"(仅 agent_call)|
 | `sub_trace_metadata` | object \| null | Sub-Trace 元数据(仅 agent_call,包含最后消息等)|
 | `self_stats` | GoalStats | 自身统计 |
@@ -1078,7 +1087,10 @@ const mainTrace = {
   goal_tree: {
     goals: [
       { id: "1", type: "normal", description: "分析问题" },
-      { id: "2", type: "agent_call", agent_call_mode: "explore", sub_trace_ids: ["abc123.A", "abc123.B"] },
+      { id: "2", type: "agent_call", agent_call_mode: "explore", sub_trace_ids: [
+        {"trace_id": "abc123.A", "mission": "JWT 方案"},
+        {"trace_id": "abc123.B", "mission": "Session 方案"}
+      ] },
       { id: "3", type: "normal", description: "完善实现" }
     ]
   },

+ 5 - 3
frontend/htmlTemplate/templateData.py

@@ -249,8 +249,10 @@ async def _watch_ws_events(trace_id: str, since_event_id: int = 0, ws_url: Optio
                     event = data.get("event")
                     if event:
                         print(f"收到事件: {event}")
-        except Exception:
-            print("WebSocket 连接断开,1 秒后重连")
+        except Exception as e:
+            import traceback
+            traceback.print_exc()
+            print(f"WebSocket 连接断开: {e},1 秒后重连")
             await asyncio.sleep(1)
 
 
@@ -291,7 +293,7 @@ if __name__ == "__main__":
             # save_ws_data_to_file(args.trace_id, args.since_event_id, args.ws_url)
     else:
         trace_list_data = generate_trace_list()
-        print(f"🐒trace_list_data: {trace_list_data}")
+        # print(f"🐒trace_list_data: {trace_list_data}")
 
         traces = trace_list_data.get("traces") or []
         # trace_id = traces[0].get("trace_id") if traces else None

File diff suppressed because it is too large
+ 0 - 13
frontend/htmlTemplate/ws_data/event.jsonl


+ 123 - 14
frontend/react-template/src/components/FlowChart/FlowChart.tsx

@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useRef, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
 import type { FC } from "react";
 import * as d3 from "d3";
 import type { Goal } from "../../types/goal";
@@ -14,6 +14,7 @@ interface FlowChartProps {
   goals: Goal[];
   msgGroups?: Record<string, Message[]>;
   onNodeClick?: (node: Goal, edge?: EdgeType) => void;
+  onSubTraceClick?: (parentGoal: Goal, entry: SubTraceEntry) => void;
 }
 
 interface LayoutNode extends d3.HierarchyPointNode<Goal> {
@@ -23,8 +24,9 @@ interface LayoutNode extends d3.HierarchyPointNode<Goal> {
 
 type TreeGoal = Goal & { children?: TreeGoal[] };
 const VIRTUAL_ROOT_ID = "__VIRTUAL_ROOT__";
+export type SubTraceEntry = { id: string; mission?: string };
 
-export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeClick }) => {
+export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeClick, onSubTraceClick }) => {
   // 确保 goals 中包含 END 节点
   const displayGoals = useMemo(() => {
     if (!goals) return [];
@@ -176,21 +178,96 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
     return [...nonVirtualLinks, ...links];
   }, [layoutData, displayGoals]);
 
-  // 节点点击:记录选中状态并回传对应边
-  const handleNodeClick = (node: LayoutNode) => {
-    setSelectedNodeId(node.data.id);
-    const nearestLink = mainLinks.find((link) => link.target.data.id === node.data.id);
-    const edge: EdgeType | undefined = nearestLink
-      ? {
-          id: `${nearestLink.source.data.id}-${nearestLink.target.data.id}`,
-          source: nearestLink.source.data.id,
-          target: nearestLink.target.data.id,
-          label: "",
+  const normalizeSubTraceEntries = (goal: Goal): SubTraceEntry[] => {
+    const raw = goal.sub_trace_ids || [];
+    return raw
+      .map((item) => {
+        if (typeof item === "string") return { id: item };
+        if (item && typeof item === "object" && "trace_id" in item) {
+          const meta = item as { trace_id?: unknown; mission?: unknown };
+          const id = typeof meta.trace_id === "string" ? meta.trace_id : "";
+          const mission = typeof meta.mission === "string" ? meta.mission : undefined;
+          return id ? { id, mission } : null;
         }
-      : undefined;
-    onNodeClick?.(node.data, edge);
+        return null;
+      })
+      .filter((entry): entry is SubTraceEntry => !!entry && entry.id.length > 0);
   };
 
+  // 节点点击:记录选中状态并回传对应边
+  const handleNodeClick = useCallback(
+    (node: LayoutNode) => {
+      setSelectedNodeId(node.data.id);
+      const nearestLink = mainLinks.find((link) => link.target.data.id === node.data.id);
+      const edge: EdgeType | undefined = nearestLink
+        ? {
+            id: `${nearestLink.source.data.id}-${nearestLink.target.data.id}`,
+            source: nearestLink.source.data.id,
+            target: nearestLink.target.data.id,
+            label: "",
+          }
+        : undefined;
+      onNodeClick?.(node.data, edge);
+    },
+    [mainLinks, onNodeClick],
+  );
+
+  const subTraceLinks = useMemo(() => {
+    if (!layoutData)
+      return [] as Array<{
+        link: d3.HierarchyPointLink<Goal>;
+        label?: string;
+        key: string;
+        onClick: () => void;
+        curveOffset?: number;
+      }>;
+    const nodeMap = new Map(layoutData.nodes.map((node) => [node.data.id, node]));
+    const orderedIds = displayGoals.map((goal) => goal.id);
+    const nextIdMap = new Map<string, string>();
+    for (let i = 0; i < orderedIds.length - 1; i += 1) {
+      nextIdMap.set(orderedIds[i], orderedIds[i + 1]);
+    }
+
+    const links: Array<{
+      link: d3.HierarchyPointLink<Goal>;
+      label?: string;
+      key: string;
+      onClick: () => void;
+      curveOffset?: number;
+    }> = [];
+
+    displayGoals.forEach((goal) => {
+      const entries = normalizeSubTraceEntries(goal);
+      if (entries.length === 0) return;
+      const sourceBase = nodeMap.get(goal.id);
+      const nextId = nextIdMap.get(goal.id);
+      const targetBase = nextId ? nodeMap.get(nextId) : undefined;
+      if (!sourceBase || !targetBase) return;
+
+      const centerIndex = (entries.length - 1) / 2;
+      entries.forEach((entry, index) => {
+        const offset = (index - centerIndex) * 60;
+        const onClick = () => {
+          if (onSubTraceClick) {
+            onSubTraceClick(goal, entry);
+          } else {
+            handleNodeClick(sourceBase);
+          }
+        };
+
+        links.push({
+          link: { source: sourceBase, target: targetBase },
+          label: entry.mission,
+          key: `${goal.id}-${entry.id}-explore-${index}`,
+          onClick,
+          curveOffset: offset,
+        });
+      });
+    });
+
+    return links;
+  }, [displayGoals, handleNodeClick, layoutData, onSubTraceClick]);
+
   // 当前选中节点的消息链
   const selectedMessages = useMemo(() => {
     if (!selectedNodeId) return [];
@@ -345,6 +422,22 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
                         );
                       })}
                   </g>
+                  {subTraceLinks.length > 0 && (
+                    <g className={styles.links}>
+                      {subTraceLinks.map((item) => (
+                        <Edge
+                          key={`subtrace-line-${item.key}`}
+                          link={item.link}
+                          label={item.label}
+                          highlighted={false}
+                          dimmed={false}
+                          onClick={item.onClick}
+                          mode="line"
+                          curveOffset={item.curveOffset}
+                        />
+                      ))}
+                    </g>
+                  )}
                   <g className={styles.nodes}>
                     {layoutData.nodes
                       .filter((node) => node.data.id !== VIRTUAL_ROOT_ID)
@@ -387,6 +480,22 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
                         );
                       })}
                   </g>
+                  {subTraceLinks.length > 0 && (
+                    <g className={styles.links}>
+                      {subTraceLinks.map((item) => (
+                        <Edge
+                          key={`subtrace-label-${item.key}`}
+                          link={item.link}
+                          label={item.label}
+                          highlighted={false}
+                          dimmed={false}
+                          onClick={item.onClick}
+                          mode="label"
+                          curveOffset={item.curveOffset}
+                        />
+                      ))}
+                    </g>
+                  )}
                   {messageOverlay && (
                     <g>
                       {messageOverlay.paths.map((p, idx) => (

+ 8 - 2
frontend/react-template/src/components/FlowChart/components/Edge.tsx

@@ -12,9 +12,10 @@ interface EdgeProps {
   dimmed: boolean;
   onClick: () => void;
   mode?: "line" | "label";
+  curveOffset?: number;
 }
 
-export const Edge: FC<EdgeProps> = ({ link, label, highlighted, dimmed, onClick, mode = "line" }) => {
+export const Edge: FC<EdgeProps> = ({ link, label, highlighted, dimmed, onClick, mode = "line", curveOffset = 0 }) => {
   const { source, target } = link;
   const truncateMiddle = (text: string, limit: number) => {
     if (text.length <= limit) return text;
@@ -31,6 +32,11 @@ export const Edge: FC<EdgeProps> = ({ link, label, highlighted, dimmed, onClick,
     const startX = sourceX + direction * nodeHalfW;
     const endX = targetX - direction * nodeHalfW;
     const midX = (startX + endX) / 2;
+
+    if (curveOffset !== 0) {
+      return `M${startX},${sourceY} C${midX},${sourceY + curveOffset} ${midX},${targetY + curveOffset} ${endX},${targetY}`;
+    }
+
     return `M${startX},${sourceY} C${midX},${sourceY} ${midX},${targetY} ${endX},${targetY}`;
   };
 
@@ -75,7 +81,7 @@ export const Edge: FC<EdgeProps> = ({ link, label, highlighted, dimmed, onClick,
     const startX = sourceX + direction * nodeHalfW;
     const endX = targetX - direction * nodeHalfW;
     const midX = (startX + endX) / 2;
-    const midY = (sourceY + targetY) / 2;
+    const midY = (sourceY + targetY) / 2 + curveOffset * 0.75;
 
     const truncated = truncateMiddle(label, 6);
 

+ 29 - 6
frontend/react-template/src/components/FlowChart/hooks/useFlowChartData.ts

@@ -81,8 +81,19 @@ export const useFlowChartData = (traceId: string | null, initialGoals: Goal[]) =
           (rawTrace && isRecord(rawTrace.goal_tree) ? rawTrace.goal_tree : undefined) ||
           {};
         const goalList = isRecord(goalTree) ? goalTree.goals : undefined;
+        console.log("%c [ goalList ]-90", "font-size:13px; background:pink; color:#bf2c9f;", goalList);
         const nextGoals = Array.isArray(goalList) ? (goalList as Goal[]) : [];
-        setGoals(nextGoals);
+        console.log("%c [ nextGoals ]-91", "font-size:13px; background:pink; color:#bf2c9f;", nextGoals);
+        setGoals((prev) => {
+          return nextGoals.map((ng) => {
+            const existing = prev.find((p) => p.id === ng.id);
+            // 保留 sub_trace_ids,如果 WebSocket 数据中缺失但本地已有
+            if (existing && existing.sub_trace_ids && !ng.sub_trace_ids) {
+              return { ...ng, sub_trace_ids: existing.sub_trace_ids };
+            }
+            return ng;
+          });
+        });
         return;
       }
 
@@ -93,7 +104,13 @@ export const useFlowChartData = (traceId: string | null, initialGoals: Goal[]) =
           const next = [...prev];
           const idx = next.findIndex((g) => g.id === goal.id);
           if (idx >= 0) {
-            next[idx] = { ...next[idx], ...goal };
+            const existing = next[idx];
+            const merged = { ...existing, ...goal };
+            // 保留 sub_trace_ids,如果 WebSocket 数据中缺失但本地已有
+            if (existing.sub_trace_ids && !merged.sub_trace_ids) {
+              merged.sub_trace_ids = existing.sub_trace_ids;
+            }
+            next[idx] = merged;
             return next;
           }
           next.push(goal as Goal);
@@ -133,14 +150,20 @@ export const useFlowChartData = (traceId: string | null, initialGoals: Goal[]) =
 
       if (event === "message_added") {
         const message = isMessage(data.message) ? data.message : isMessage(raw.message) ? raw.message : null;
-        if (!message) return;
-        setMessages((prev: Message[]) => [...prev, message]);
-        updateMessageGroups(message);
+        if (message) {
+          setMessages((prev) => {
+            const next = [...prev, message];
+            next.sort((a, b) => messageSortKey(a) - messageSortKey(b));
+            return next;
+          });
+          updateMessageGroups(message);
+        }
       }
     },
-    [updateMessageGroups],
+    [messageSortKey, updateMessageGroups],
   );
 
+  // 主 Trace 连接
   const wsOptions = useMemo(() => ({ onMessage: handleWebSocketMessage }), [handleWebSocketMessage]);
   const { connected } = useWebSocket(traceId, wsOptions);
 

+ 2 - 0
frontend/react-template/src/types/goal.ts

@@ -8,6 +8,8 @@ export interface Goal {
   created_at: string;
   completed_at?: string;
   metadata?: Record<string, unknown>;
+  agent_call_mode?: string;
+  sub_trace_ids?: Array<string | { trace_id: string; mission?: string }>;
 }
 
 export interface BranchContext {

Some files were not shown because too many files changed in this diff