Fan-in、trace 与恢复路径 深度解析
这一页拆 collect_reports 如何避免轮询风暴、AgentBus 如何保留 session 边界,以及 trace/checkpoint 如何让长任务可恢复。
导入链与契约
报告收集不是简单 await all。sources/frontieragent/plugins/tools/collect_reports.py:130 的工具文档明确 FIRST completion 语义;sources/frontieragent/plugins/tools/collect_reports.py:139 到 sources/frontieragent/plugins/tools/collect_reports.py:169 把 drain、状态分类、running wait 和 status-only block 全部写进模型可见说明。
130@tool 131async def collect_reports(timeout: int = 1800) -> str: 132 """Collect reports from any sub-agents that have finished their tasks. 133 134 Reports also drain automatically between turns when agents finish 135 *during* a turn, but agents still running at the start of a new turn 136 won't appear unless you call this. Block-wait here is the right move 137 whenever you need a running agent's output to proceed. 138 139 Behaviour: 140 1. Non-blocking drain: any sub-agent that already finished returns 141 its ``<report>`` immediately, regardless of ``timeout``. 142 2. If the drain found nothing, classify the session state: 143 * no agents at all → ``"create_subagent first"``. 144 * agents created but never assigned → ``[status] no_work_queued``, 145 call ``assign_task`` (do NOT poll again). 146 * all submitted work already collected → ``[status] all_collected``, 147 synthesize or assign a follow-up (do NOT poll again). 148 * at least one session running → wait up to ``timeout`` seconds 149 for the FIRST completion, then drain any others that 150 completed during the wait. 151 152 Reports come back as 153 ``<report agent="NAME" status="complete|incomplete|failed|paused" 154 reason="...">…</report>`` — treat ``incomplete`` / ``failed`` as 155 partial evidence, not a final answer. 156 157 Args: 158 timeout: Max seconds to wait for the first completion when 159 something is genuinely running. Default 1800 (30 min). 160 Sub-agents on hard BrowseComp-style questions routinely 161 take 3-10 minutes — short timeouts are a poll, not a 162 wait. Pass a smaller value only when you specifically 163 want a cheap poll. 164 165 Returns: 166 One or more ``<report>`` blocks plus a status line. Status-only 167 outcomes (nothing to wait for / timeout) come back as a single 168 ``<report agent="orchestrator" …>`` block carrying the 169 directive, followed by the ``[status]`` line. 170 """
核心实现,逐段拆
▸状态短路防止 collect_reports 轮询风暴
collect_reports 有专门的 churn counter。sources/frontieragent/plugins/tools/collect_reports.py:69 记录连续同状态次数,sources/frontieragent/plugins/tools/collect_reports.py:86 渲染 status-only return,sources/frontieragent/plugins/tools/collect_reports.py:111 超过阈值后直接告诉 Agent 停止轮询并 finalize 或派发具体 follow-up。
69def _bump_status_churn(scope: Any, state: str) -> int: 70 """Increment and return the consecutive count for ``state``.""" 71 md = getattr(scope, "metadata", None) 72 if md is None: 73 return 1 74 prev_state, count = md.get(_STATUS_CHURN_KEY) or ("", 0) 75 count = count + 1 if prev_state == state else 1 76 md[_STATUS_CHURN_KEY] = (state, count) 77 return count 78 79 80def _reset_status_churn(scope: Any) -> None: 81 md = getattr(scope, "metadata", None) 82 if md: 83 md.pop(_STATUS_CHURN_KEY, None) 84 85 86def _status_notice( 87 scope: Any, 88 state: str, 89 body: str, 90 *, 91 extra_status: str | None = None, 92 track_churn: bool = True, 93) -> str: 94 """Render a status-only return: ``<report>`` block + ``[status]`` line. 95 96 Every short-circuit return goes through here so SDK consumers 97 parsing ``tool_finished.result_preview`` always find a well-formed 98 ``<report>`` block (see 99 :func:`workflows.agent_team._report_format.format_status_report_block`) 100 — bare free-form status text used to be pasted back to end users 101 verbatim with a parse-failure marker. 102 103 With ``track_churn`` (idle states only), the same state returned 104 more than :data:`_STATUS_CHURN_LIMIT` times in a row swaps the body 105 for a hard stop-polling directive and tags 106 ``reason="<state>_stop_polling"`` so trace consumers can spot the 107 churn at a glance. 108 """ 109 reason = state 110 if track_churn: 111 count = _bump_status_churn(scope, state) 112 if count > _STATUS_CHURN_LIMIT: 113 reason = f"{state}_stop_polling" 114 body = ( 115 f"This is consecutive '{state}' notice #{count}. STOP " 116 "calling collect_reports — repeating it cannot return " 117 "anything new. Call finalize_answer NOW with your best " 118 "synthesis of the reports already collected, or " 119 "assign_task with a concrete follow-up question if (and " 120 "only if) one is genuinely needed." 121 ) 122 else: 123 _reset_status_churn(scope) 124 out = f"{format_status_report_block(reason, body)}\n\n[status] {state}" 125 if extra_status: 126 out += f"\n{extra_status}" 127 return out 128
▸Trace 与 checkpoint 把中断变成可复盘状态
TraceObserver 是 append-only JSONL,sources/frontieragent/apodex/trace.py:49 记录 LLM response,sources/frontieragent/apodex/trace.py:57 记录 tool result,sources/frontieragent/apodex/session.py:474 每回合 checkpoint。对长程 Agent 来说,这比最终答案更重要,因为它让失败能定位到具体 turn 和 tool call。
30class TraceObserver(BaseObserver): 31 """Awaited (critical) so trace lines stay ordered; never returns an 32 intervention. Append-only JSONL; failures are swallowed (a broken trace 33 must not break a run).""" 34 35 critical = True 36 37 def __init__(self, path: str, *, mode: str = "", cwd: str = "") -> None: 38 self.path = path 39 self._write({"t": "start", "mode": mode, "cwd": cwd}) 40 41 def _write(self, rec: dict[str, Any]) -> None: 42 try: 43 with open(self.path, "a", encoding="utf-8") as f: 44 f.write(json.dumps(rec, ensure_ascii=False, default=str) + "\n") 45 f.flush() 46 except Exception: 47 pass 48 49 async def on_llm_response(self, ctx: TurnContext) -> Intervention | None: 50 self._write({ 51 "t": "llm", "turn": ctx.turn, 52 "text": ctx.ai_text or "", 53 "tool_calls": list(ctx.tool_calls or []), 54 }) 55 return None 56 57 async def on_tool_result( 58 self, ctx: TurnContext, result: ToolResult, 59 ) -> ToolResult | None: 60 self._write({ 61 "t": "tool", "turn": ctx.turn, "name": result.name, 62 "args": dict(result.args or {}), 63 "ms": result.duration_ms, "is_error": result.is_error, 64 "result": result.result or "", 65 }) 66 return None 67 68 async def on_loop_end(self, result: AgentLoopResult) -> None: 69 self._write({ 70 "t": "end", "stopped_by": result.stopped_by, 71 "turns": result.turns_used, "tool_calls": result.tool_calls_count, 72 }) 73 return None
关键流程
▸恢复依赖边界事件和交付路径
恢复路径的关键是把生命周期和产物路径都留在状态里:sources/frontieragent/frontier_agent/components/agent_bus/runtime.py:143 记录 session submit event,sources/frontieragent/apodex/session.py:493 起把 deliverable paths 重新注入下一轮上下文。
AgentBus 自己也保留 session 边界。sources/frontieragent/frontier_agent/components/agent_bus/runtime.py:118 的 close_session_boundary_aborted 会在取消或错误时补 abort stub,sources/frontieragent/frontier_agent/components/agent_bus/runtime.py:143 和 sources/frontieragent/frontier_agent/components/agent_bus/runtime.py:217 分别写 submit/completed lifecycle event。
边界 / 失败模式
- 剩余 wall-clock 太低时跳过阻塞等待:
sources/frontieragent/plugins/tools/collect_reports.py:181到sources/frontieragent/plugins/tools/collect_reports.py:202。 - 未知 stop reason 不会被标为 complete:
sources/frontieragent/frontier_agent/components/agent_bus/fan_in.py:195。 - trace 写入失败不破坏主任务:
sources/frontieragent/apodex/trace.py:41到sources/frontieragent/apodex/trace.py:47。 - resumed follow-up 会收到已有 deliverable 路径:
sources/frontieragent/apodex/session.py:493到sources/frontieragent/apodex/session.py:526。