frontieragentAgent
§ · Nested detail page

Fan-in、trace 与恢复路径 深度解析

这一页拆 collect_reports 如何避免轮询风暴、AgentBus 如何保留 session 边界,以及 trace/checkpoint 如何让长任务可恢复。

§ 00 · IMPORT CHAIN

导入链与契约

报告收集不是简单 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 全部写进模型可见说明。

src/plugins/tools/collect_reports.py · L130-170Python
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    """
§ 01 · CORE IMPL

核心实现,逐段拆

▸状态短路防止 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。

src/plugins/tools/collect_reports.py · L69-128Python
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。

src/apodex/trace.py · L30-73Python
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
§ 02 · KEY FLOW

关键流程

collect_reports ├─ wait_any_session(timeout=0) drain ready ├─ classify idle/running/all_collected ├─ heartbeat to TUI observers ├─ wall-clock clamp └─ report blocks with completion status session loop ├─ on_turn checkpoint ├─ JSONL trace └─ resume with exact deliverable paths

▸恢复依赖边界事件和交付路径

恢复路径的关键是把生命周期和产物路径都留在状态里: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。

§ 03 · EDGE CASES

边界 / 失败模式

  • 剩余 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。
Sub-page · 2026-08-26 Source · ApodexAI/FrontierAgent ↑ frontieragent 主页面