【Bug已解决】Subagent activity is invisible from the main thread list, spinner only reflects main thread 解决方案原始报错Subagent activity is invisible from the main thread list, spinner only reflects main thread 场景一个任务会派生出多个子智能体subagent并行干活。主界面只显示主线程的进度转圈spinner子智能体在干什么、是否卡住、是否完成在主任务列表里完全看不到。用户只能干等主线程转圈无法判断子任务是不是已经挂了。 关键词子任务可见性、任务树、状态聚合、事件总线、进度 spinner 聚合。一、现象长什么样主任务列表里有一行任务 A —— 进行中转圈。但任务 A 内部派了 3 个子智能体分别查资料、写代码、跑测试。用户看到的现象是列表里只有任务 A一行没有任务 A / 子任务1 查资料之类转圈只反映主线程是否空闲子智能体忙不忙它不体现如果某个子智能体卡死主线程还在等它转圈一直转用户无从知道其实卡在子任务3子智能体完成了界面也不会有任何独立提示只有等全部结束后主行才变完成。这是典型的监控盲区系统把主线程当成唯一可观测对象忽略了它派生出的工作树。用户失去了对并行子任务的可观测性也就失去了判断进度和定位卡点的能力。二、背景为什么只看到主线程任务系统通常这样实现每个顶层任务有一个状态对象spinner绑定到这个对象。子智能体往往是函数内部spawn出来的没有注册到任何全局任务登记表registryUI 也只订阅顶层任务。于是子智能体的生命周期对 UI 不可见子智能体的进度事件发到了别处或被直接丢弃主线程的 spinner 由主线程是否在等驱动而在等子任务和真的空闲在它看来没区别。要让子任务可见必须建立任务树 事件总线让所有层级的任务都向同一个可见性系统汇报。三、根因没有任务登记表事件不向上聚合根因拆解子任务未登记派生子智能体时没有把它加进任务树UI 根本不知道它的存在。事件不发总线子任务的状态变化开始/进展/完成/失败只存在局部变量里没发到全局事件总线。聚合缺失主行 spinner 只反映主线程没把子任务的活跃度聚合进来。卡点不可定位因为没树、没事件子任务卡死时无法在 UI 标红/标注。下面用最小模型复现只反映主线程再给修复。四、最小可运行复现用线程模拟主任务派生子任务。错误写法只跟踪主线程是否结束子线程状态没人管。import threading, time class Task: def __init__(self, name): self.name name self.done False def run(self): time.sleep(0.3) self.done True def main_task(): t Task(主任务) subs [] for i in range(3): st threading.Thread(targetlambda: time.sleep(0.5 i * 0.2)) st.start() subs.append(st) # 主线程只等自己这点活不等子任务 t.run() # 错误UI 只看 t.done子任务状态完全未知 return t.done if __name__ __main__: ok main_task() print(主任务完成:, ok) # 此时子任务可能还在跑但没有任何可见状态这里main_task返回时子任务还在后台跑但系统没有任何结构记录它们UI 自然看不到。五、方案建立任务树登记表第一层所有任务含子任务都注册到一个全局树每个节点有id / parent / status / progressimport threading, time, uuid from dataclasses import dataclass, field from typing import Dict, List, Optional dataclass class TaskNode: id: str name: str parent: Optional[str] status: str pending # pending/running/done/failed progress: float 0.0 class TaskRegistry: def __init__(self): self._nodes: Dict[str, TaskNode] {} def spawn(self, name: str, parent: Optional[str] None) - str: tid uuid.uuid4().hex[:8] self._nodes[tid] TaskNode(idtid, namename, parentparent, statusrunning) return tid def update(self, tid: str, status: str None, progress: float None): n self._nodes.get(tid) if n is None: return if status: n.status status if progress is not None: n.progress progress def children(self, tid: str) - List[TaskNode]: return [n for n in self._nodes.values() if n.parent tid] def all(self) - List[TaskNode]: return list(self._nodes.values()) if __name__ __main__: reg TaskRegistry() root reg.spawn(主任务) c1 reg.spawn(查资料, parentroot) c2 reg.spawn(写代码, parentroot) print(可见任务数:, len(reg.all())) # 3主2子全部可见 print(主任务的子任务:, [n.name for n in reg.children(root)])现在 UI 只要遍历registry.all()就能列出任务树子任务不再隐身。六、方案事件总线让状态变化可订阅第二层子任务状态变化通过事件总线广播UI 订阅即可实时刷新不必轮询class EventBus: def __init__(self): self._subs [] def subscribe(self, fn): self._subs.append(fn) def publish(self, event: dict): for fn in self._subs: fn(event) def make_worker(reg: TaskRegistry, bus: EventBus, tid: str, work: float): def run(): for step in range(1, 6): time.sleep(work / 5) reg.update(tid, progressstep / 5) bus.publish({task: tid, progress: step / 5}) reg.update(tid, statusdone) bus.publish({task: tid, status: done}) return threading.Thread(targetrun) if __name__ __main__: reg TaskRegistry() bus EventBus() bus.subscribe(lambda e: print([事件], e)) root reg.spawn(主任务) t1 reg.spawn(查资料, parentroot) w make_worker(reg, bus, t1, 0.5) w.start() w.join()每个进度/状态变化都通过bus.publish发出UI 订阅后就能实时显示子任务进展不再只有主线程转圈。七、方案聚合 spinner 反映整体活跃度第三层主行 spinner 不应只看主线程而应根据整棵子树是否还有活跃节点来聚合def is_active(reg: TaskRegistry, tid: str) - bool: node reg._nodes.get(tid) if node is None: return False if node.status in (running, pending): return True # 任一子节点活跃则整体活跃 return any(is_active(reg, c.id) for c in reg.children(tid)) def aggregate_progress(reg: TaskRegistry, tid: str) - float: node reg._nodes.get(tid) if node is None: return 0.0 children reg.children(tid) if not children: return node.progress # 子树平均进度 return sum(aggregate_progress(reg, c.id) for c in children) / len(children) if __name__ __main__: reg TaskRegistry() bus EventBus() root reg.spawn(主任务) t1 reg.spawn(查资料, parentroot) t2 reg.spawn(写代码, parentroot) reg.update(t1, statusdone, progress1.0) reg.update(t2, statusrunning, progress0.5) print(主任务是否活跃:, is_active(reg, root)) # True子任务2还在跑 print(聚合进度:, round(aggregate_progress(reg, root), 2)) # 0.75is_active递归检查子树只要还有一个子任务在跑主行 spinner 就继续转aggregate_progress给出整棵树的真实进度比只看主线程准确得多。八、验证把任务树与聚合锁进测试def test_subtasks_visible_in_registry(): reg TaskRegistry() root reg.spawn(主任务) reg.spawn(子1, parentroot) reg.spawn(子2, parentroot) assert len(reg.children(root)) 2 def test_aggregate_active_while_child_runs(): reg TaskRegistry() root reg.spawn(主任务) c reg.spawn(子, parentroot) reg.update(c, statusrunning) assert is_active(reg, root) is True reg.update(c, statusdone) # 主任务本身还没标 done assert is_active(reg, root) is True if __name__ __main__: test_subtasks_visible_in_registry() test_aggregate_active_while_child_runs() print(子任务可见性与聚合测试通过。)九、排查清单子任务活动不可见按顺序查任务登记表派生子任务时是否注册到全局任务树UI 能否枚举到它们事件总线子任务状态变化是否发到可订阅的总线还是只留在局部变量树结构每个任务是否记录parent能否按树展开聚合主行 spinner / 进度是否聚合子树还是只看主线程卡点定位子任务失败时 UI 能否标红/标注还是只有整棵树卡住生命周期子任务结束/失败是否同步更新父节点状态订阅清理UI 关闭时是否退订总线避免泄漏十、小结子任务活动不可见、spinner 只反映主线程是可观测性只建在主线程上导致的监控盲区。修复三层登记所有层级的任务都进全局任务树UI 能枚举整棵工作树广播状态变化走事件总线UI 实时刷新不靠轮询聚合主行 spinner / 进度递归聚合子树整体活跃度与真实进度一目了然。核心原则派生的每一点工作都应当是可见、可订阅、可聚合的。做到这三点用户不再被一个永远在转却不知在等什么的圈圈吊着子任务卡死也能立刻在界面上定位到具体哪一支。