frontieragentAgent
§ 00 · Source-level Observatory

frontieragent 长程研究与文件工作 Agent Runtime

FrontierAgent 把终端 TUI、Stateful ReAct、Agent Team、任务沙箱、审批、trace 和 benchmark runner 拆成几层可复用模块。它值得拆,是因为源码里真的有持久 sub-agent session、fan-in 状态机和面向长任务的恢复路径,而不只是 README 里的多智能体图。

★ 638
GitHub Stars
162K
LOC
601
Files
2026-08-26
Last commit
TUI / CLI │ ▼ BenchmarkSession runtime ├─ Stateful ReAct └─ Agent Team coordinator ├─ create_subagent ├─ assign_task └─ collect_reports ▼ Sandboxed tools · trace · checkpoints
§ 00 · OVERVIEW

它解决什么,凭什么值得拆

FrontierAgent 的主线不是把一个聊天机器人套上终端皮肤,而是把长程研究和文件工作拆成运行时、工作流、工具、观察者和 UI。入口层在 sources/frontieragent/apodex/task_runner.py:427 明确说原生 workflow 需要注册 AgentBus、workflow roles 和完整工具表;这意味着 Agent Team 不是 generic ReAct 假装并行,而是另一条真正的 pipeline。

源码里最值得关注的是它对失败的态度:任务派发有深度和并发 guard sources/frontieragent/frontier_agent/components/agent_bus/bus.py:431,报告收集会区分 no_work_queued、running、all_collected sources/frontieragent/plugins/tools/collect_reports.py:139,审批层在运行前展示 diff 或 command sources/frontieragent/apodex/observers.py:469,trace observer 逐行写 JSONL sources/frontieragent/apodex/trace.py:30。这是一套偏生产运行的 agent harness。

▸核心数据

架构模式
分层 runtime:`apodex/` 负责 CLI/TUI/session,`workflows/` 负责 ReAct 与 Agent Team,`frontier_agent/components/agent_bus/` 负责异步 sub-agent bus,`plugins/tools/` 负责工具契约与沙箱能力。
核心抽象
核心抽象是 AgentBus + SubAgentSession + workflow profile。AgentBus 保存 jobs、sessions、spawn guard、event sink 和共享 artifact pool;profile 决定 reporter、模型、工具和 workflow 绑定。
性能/精度数据
README 展示 Apodex 1.1 benchmark 图,源码侧更硬的数字是守护逻辑中的经验值:过度复用 sub-agent session 会让模型被旧结论锚定,所以单 session 上限固定为 5。
下游依赖
下游是终端 TUI、Docker/native sandbox、SGLang/local endpoint、FrontierSearchBench/FrontierChallenge,以及可复用的 benchmark runner。
本文导航 — 6 个章节依次:概述 → 架构图 → 核心模块 → 关键流程 → 测试与验证 → 风险。子页面在每个 H3 末尾以 深度阅读 → 入口展开。
§ 01 · ARCHITECTURE

从 TUI 到 Workflow 的真实边界

从入口看,FrontierAgent 把用户的终端任务包装进 workflow runtime,再让 workflow 决定是单 agent 还是 Agent Team。这里关键不是 UI,而是注册边界:sources/frontieragent/apodex/task_runner.py:443 引入 SteerInbox,sources/frontieragent/apodex/task_runner.py:445 引入 BenchmarkSession,sources/frontieragent/apodex/task_runner.py:461 把 profile、cwd、observers 和 session_id 放入 metadata,sources/frontieragent/apodex/task_runner.py:507 才真正进入 runtime.run。

▸系统拓扑

User task │ ▼ apodex.task_runner._run_native_workflow │ metadata(profile, cwd, observers, session_id) ▼ BenchmarkSession.run(pipeline_id) ├─ workflows/stateful_react_agent └─ workflows/agent_team ├─ AgentBus sessions ├─ create_subagent / assign_task └─ collect_reports fan-in ▼ TraceObserver + approvals + checkpoints

▸模块清单

这几个模块构成了项目的真实骨架:终端入口把 workflow 跑在 BenchmarkSession 中 sources/frontieragent/apodex/task_runner.py:427;profile loader 处理 YAML、inline profile 和 overrides sources/frontieragent/workflows/agent_team/profile.py:132;AgentBus 保存 jobs 与 session 状态 sources/frontieragent/frontier_agent/components/agent_bus/bus.py:300;assign_task 用 output_paths 作为发布授权 sources/frontieragent/plugins/tools/assign_task.py:167;collect_reports 是 fan-in 的模型可见工具 sources/frontieragent/plugins/tools/collect_reports.py:130;TerminalObserver 在 plan mode 前拦截 mutating tool sources/frontieragent/apodex/observers.py:437。

apodex runtime
CLI/TUI/session 负责 profile、approver、trace、checkpoint 与 steering。证据在 sources/frontieragent/apodex/session.py:474 和 sources/frontieragent/apodex/trace.py:41。
Agent Team workflow
Agent Team 用 profile 决定 reporter backend、LLM 配置和 overrides,见 sources/frontieragent/workflows/agent_team/profile.py:68 与 sources/frontieragent/workflows/agent_team/profile.py:132。
AgentBus
Bus 管理 jobs、sessions、spawn guard、event sink 和 artifact aggregate,见 sources/frontieragent/frontier_agent/components/agent_bus/bus.py:322 到 sources/frontieragent/frontier_agent/components/agent_bus/bus.py:335。
Tool layer
工具 metadata 标注 read-only、destructive、concurrency-safe、timeout 和 overflow 策略,见 sources/frontieragent/plugins/tools/meta.py:15。
Fan-in
报告收集把 complete、incomplete、failed、paused 明确编码,见 sources/frontieragent/frontier_agent/components/agent_bus/fan_in.py:147。
Tests
测试覆盖 profile、provider、workflow mode 和 TUI busy steering,见 sources/frontieragent/apodex/tests/test_profiles.py:86 与 sources/frontieragent/apodex/tests/test_tui.py:142。

▸关键数据结构

src/frontier_agent/components/agent_bus/bus.py · L300-335Python
300    """Async job model for sub-agent dispatch.
301
302    Core API: submit() / collect() / abort().
303
304    If a SpawnGuard is attached, submit() enforces concurrency/depth/token
305    limits. Without a guard, only basic depth checking applies.
306    """
307
308    def __init__(
309        self,
310        *,
311        event_sink: EventSink | None = None,
312        pause_check_factory: PauseCheckFactory | None = None,
313        agent_registry: AgentRegistry | None = None,
314        resource_manager: Any = None,
315        session_history_dir: Path | None = None,
316    ) -> None:
317        """Construct the bus with optional injected services.
318
319        Missing services fall back to the registry. ``session_history_dir``
320        enables disk offload for messages removed by eager trimming.
321        """
322        self._jobs: dict[str, JobEntry] = {}
323        self._counter: int = 0
324        self._spawn_guard: SpawnGuard | None = None
325        self._sub_agent_profiles: dict[str, dict[str, SubAgentProfile]] = {}
326        self._sessions: dict[str, SubAgentSession] = {}
327        # Per-task evidence / assertion pool — populated when main agents
328        # (or collect_reports) harvest completed SubAgentResults, drained
329        # by the pipeline node at phase end for the report node to consume.
330        self._task_aggregates: dict[str, dict[str, list[dict[str, Any]]]] = {}
331        self._event_sink_injected: EventSink | None = event_sink
332        self._pause_check_factory: PauseCheckFactory | None = pause_check_factory
333        self._agent_registry_injected: AgentRegistry | None = agent_registry
334        self._resource_manager_injected: Any = resource_manager
335        self._session_history_dir: Path | None = session_history_dir
深度阅读 · Agent Team profile 与 session runtime
§ 02 · CORE MODULES

Agent Team 与工具契约

▸Profile loader 是工作流配置的第一道门

Agent Team 不把所有参数硬编码进 prompt,而是通过 profile loader 装载 YAML 或 inline profile,再做 env-var resolution、provider credential 注入、overrides deep merge 和 blacklist merge。sources/frontieragent/workflows/agent_team/profile.py:132 到 sources/frontieragent/workflows/agent_team/profile.py:178 显示了这个顺序;sources/frontieragent/workflows/agent_team/profile.py:88 的 deep_merge_overrides 说明 overrides 不会污染 base profile。

src/workflows/agent_team/profile.py · L88-120Python
88def deep_merge_overrides(
89    base: dict[str, Any], overrides: dict[str, Any],
90) -> dict[str, Any]:
91    """Deep-merge ``overrides`` onto ``base`` without mutating either.
92
93    Semantics chosen to match SDK caller intent (override one nested
94    knob, leave siblings alone):
95
96    - Nested dicts merge recursively (sibling keys preserved).
97    - Lists are replaced wholesale — partial list merging is almost
98      never what callers want (e.g. ``fallback_api_keys: [new_key]``
99      should *replace* the existing key list, not append).
100    - ``None`` in the override **removes** the key, so callers can
101      disable a defaulted-on feature.
102    - Scalar override replacing a dict in base wins — caller asked
103      for that shape, however unusual.
104    """
105    if not overrides:
106        # Returning a deep copy preserves the contract that the caller
107        # can mutate the result without leaking back into ``base``.
108        return _deep_copy(base)
109
110    out: dict[str, Any] = _deep_copy(base)
111    for key, value in overrides.items():
112        if value is None:
113            out.pop(key, None)
114            continue
115        base_value = out.get(key)
116        if isinstance(base_value, dict) and isinstance(value, dict):
117            out[key] = deep_merge_overrides(base_value, value)
118        else:
119            out[key] = _deep_copy(value)
120    return out

▸Assignment manifest 把发布权限写进工具 schema

Agent Team 的 assign_task 不是简单把 prompt 扔给 worker。sources/frontieragent/plugins/tools/assign_task.py:155 解释旧 publish 字段为什么退场,sources/frontieragent/plugins/tools/assign_task.py:185 到 sources/frontieragent/plugins/tools/assign_task.py:208 把 output_paths 明确设为发布授权,避免多个 sub-agent 同时写最终产物。

src/plugins/tools/assign_task.py · L155-208Python
155# ``publish`` used to be the authorization field and ``output_paths`` its
156# attachment. They were never independent: ``validate_publish_contract``
157# rejected true-without-paths and paths-without-true alike, so the boolean
158# carried nothing the manifest did not already carry. Requiring it only added a
159# key the coordinator omitted on roughly a quarter of its assignments (60/236
160# and 63/225 across the two arms of the 30-task APEX replay), where the default
161# silently absorbed it as false -- and an omission next to a correct manifest
162# was a hard rejection rather than a publisher. The manifest is now the grant;
163# ``publish`` survives only so callers that still send it are not rejected.
164#
165# NOTE: this class's docstring is rendered verbatim into the model-facing tool
166# schema, so keep it to what the coordinator needs to read.
167class AgentTeamAssignmentSpec(AssignmentSpec):
168    """One assignment. Carrying ``output_paths`` is what authorizes it to write
169    those ``/outputs`` paths; without them the task runs workspace-only."""
170
171    model_config = ConfigDict(extra="forbid")
172
173    publish: StrictBool | None = Field(
174        default=None,
175        # Marked deprecated in the advertised schema only. Pydantic's own
176        # ``deprecated=True`` warns on every attribute read, and
177        # ``validate_publish_contract`` below reads it twice per assignment --
178        # that is a DeprecationWarning per validated task in every run.
179        json_schema_extra={"deprecated": True},
180        description=(
181            "Deprecated, omit it. output_paths alone grants publication. If "
182            "sent it must agree with the manifest."
183        ),
184    )
185    output_paths: list[StrictStr] = Field(
186        default_factory=list,
187        description=(
188            "Exact absolute final file paths under /outputs. Supplying them IS "
189            "the publication grant, so set them on exactly one final-publisher "
190            "assignment and omit them on every other. Pass a JSON array even "
191            "for one path."
192        ),
193    )
194    replace_manifest: StrictBool = Field(
195        default=False,
196        description=(
197            "Set true only on a follow-up to the existing publisher when the "
198            "required final output formats genuinely changed."
199        ),
200    )
201
202    @property
203    def can_publish(self) -> bool:
204        """Whether this assignment may write ``/outputs``.
205
206        Derived from the manifest alone: a path list is the grant.
207        """
208        return bool(self.output_paths)
深度阅读 · 工具权限、审批与沙箱契约
§ 03 · KEY FLOW

任务派发、收集与恢复

任务流的关键在两侧:派发时要限制深度、预算和 session 污染;收集时要避免无意义轮询并保留 partial evidence。sources/frontieragent/frontier_agent/components/agent_bus/bus.py:431 是 submit 非阻塞入口,sources/frontieragent/plugins/tools/collect_reports.py:130 是模型可见 fan-in 入口,sources/frontieragent/frontier_agent/components/agent_bus/fan_in.py:147 定义报告状态分类。

▸AgentBus 派发路径

assign_task │ validates assignment / publish manifest ▼ AgentBus.submit_task_to_session │ session boundary + job metadata ▼ _run_and_finalize │ SpawnGuard + runtime observers ▼ SubAgentResult queued for collect_reports

▸报告收集与状态短路

collect_reports(timeout) ├─ drain ready reports ├─ classify no_subagents / no_work_queued / all_collected ├─ wait first completion when running └─ render

▸模型可见 fan-in 契约

collect_reports 的 docstring 本身就是工具契约。它在 sources/frontieragent/plugins/tools/collect_reports.py:130 进入工具定义,随后把“已有结果立即 drain、没有任务时不要轮询、运行中等待首个完成、partial evidence 继续可用”写成模型可读规则。

src/plugins/tools/collect_reports.py · L130-155Python
130async def collect_reports(timeout: int = 1800) -> str:
131    """Collect reports from any sub-agents that have finished their tasks.
132
133    Reports also drain automatically between turns when agents finish
134    *during* a turn, but agents still running at the start of a new turn
135    won't appear unless you call this. Block-wait here is the right move
136    whenever you need a running agent's output to proceed.
137
138    Behaviour:
139      1. Non-blocking drain: any sub-agent that already finished returns
140         its ``<report>`` immediately, regardless of ``timeout``.
141      2. If the drain found nothing, classify the session state:
142         * no agents at all → ``"create_subagent first"``.
143         * agents created but never assigned → ``[status] no_work_queued``,
144           call ``assign_task`` (do NOT poll again).
145         * all submitted work already collected → ``[status] all_collected``,
146           synthesize or assign a follow-up (do NOT poll again).
147         * at least one session running → wait up to ``timeout`` seconds
148           for the FIRST completion, then drain any others that
149           completed during the wait.
150
151    Reports come back as
152    ``<report agent="NAME" status="complete|incomplete|failed|paused"
153    reason="...">…</report>`` — treat ``incomplete`` / ``failed`` as
154    partial evidence, not a final answer.
155

▸Trace 是低成本恢复日志

TraceObserver 没有引入数据库或外部服务,而是在 sources/frontieragent/apodex/trace.py:30 开始写 append-only JSONL,并在 sources/frontieragent/apodex/trace.py:41 吞掉写入异常,保证观测失败不会放大成任务失败。

src/apodex/trace.py · L30-57Python
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(
深度阅读 · Fan-in、trace 与恢复路径
§ 04 · TESTS & VALIDATION

测试覆盖与运行约束

测试不是只覆盖 CLI happy path。profile 测试验证内置 react / agent_team mode 必须绑定真实 workflow profile;TUI 测试用 fake session 驱动 busy steering 和 transcript stream,保证 UI 层不会把运行中的输入误当第二个任务。

▸测试覆盖

关键断言在 sources/frontieragent/apodex/tests/test_profiles.py:86:TUI 不能用 generic ReAct 假装 Agent Team;sources/frontieragent/apodex/tests/test_profiles.py:103 验证本地 OpenAI-compatible provider 路径;sources/frontieragent/apodex/tests/test_tui.py:142 通过 Textual test harness 验证任务启动、流式输出和 busy-time steering。

tests/test_profiles.py · L86-120Python
86def test_native_workflow_modes_are_explicit_and_use_shipped_profiles(monkeypatch):
87    """The TUI must not silently emulate a team with its generic ReAct loop."""
88    monkeypatch.setenv("OPENAI_API_KEY", "test")
89    monkeypatch.setenv("OPENAI_BASE_URL", "https://example.test/v1")
90    monkeypatch.setenv("OPENAI_MODEL", "test-model")
91
92    react = P.get_profile("react")
93    team = P.get_profile("agent_team")
94
95    assert (react.workflow, react.workflow_profile) == (
96        "stateful-react-agent", "tui",
97    )
98    assert (team.workflow, team.workflow_profile) == (
99        "agent_team", "tui",
100    )
101
102
103def test_native_workflow_modes_accept_local_openai_compatible_provider(monkeypatch):
104    """The GPU Compose path needs no real API key for its local SGLang server."""
105    monkeypatch.setenv("OPENAI_PROVIDER", "local")
106    monkeypatch.setenv("OPENAI_API_KEY", "EMPTY")
107    monkeypatch.setenv("OPENAI_BASE_URL", "http://model:30000/v1")
108    monkeypatch.setenv("OPENAI_MODEL", "local-model")
109    monkeypatch.setenv("OPENAI_CONTEXT_WINDOW", "32768")
110    monkeypatch.setenv("OPENAI_MAX_TOKENS", "8192")
111
112    for mode in ("react", "agent_team"):
113        profile = P.get_profile(mode)
114        assert profile.provider == "local"
115        assert profile.runtime_config(profile.model_config, mode=mode).ok
116        assert profile.model_config.api_key == "EMPTY"
117        assert profile.model_config.base_url == "http://model:30000/v1"
118        assert profile.model_config.model == "local-model"
119        assert profile.model_config.context_window == 32768
120        assert profile.model_config.max_tokens == 8192

▸已知边界 / 失败模式

  • Session 复用被限制:sources/frontieragent/plugins/tools/assign_task.py:56 说明 stale tool stubs 和旧结论会污染后续任务,所以 MAX_TASKS_PER_SESSION 固定为 5。
  • Plan mode 先拦截 mutating tool:sources/frontieragent/apodex/observers.py:437 到 sources/frontieragent/apodex/observers.py:457 保证未批准计划时不能写文件。
  • 报告状态不能随便标 complete:sources/frontieragent/frontier_agent/components/agent_bus/fan_in.py:147 的 classify_completion 把未知 stop reason 归为 incomplete。
  • Trace 写入失败不会打断任务:sources/frontieragent/apodex/trace.py:41 到 sources/frontieragent/apodex/trace.py:47 吞掉异常,换取主流程可用性。
§ 05 · HIGHLIGHTS

工程亮点

Agent Team 不是伪并行
入口层明确通过 workflow profile 跑 BenchmarkSession,sources/frontieragent/apodex/task_runner.py:507 之后才进入 pipeline,而不是 generic loop 里摆几个工具名。
发布授权细粒度
output_paths 即 grant,sources/frontieragent/plugins/tools/assign_task.py:185 到 sources/frontieragent/plugins/tools/assign_task.py:208 让最终写入权限跟 manifest 绑定。
失败可解释
fan-in 的 incomplete note 表在 sources/frontieragent/frontier_agent/components/agent_bus/fan_in.py:63,能告诉主 Agent 是 max_turns、wall_deadline 还是 repeated_tool_calls。
TUI 支持中途 steering
测试在 sources/frontieragent/apodex/tests/test_tui.py:163 验证 busy 时输入被放入 SteerInbox,而不是启动第二个任务。
§ 06 · RISKS & GAPS

技术亮点之外,别忽略这些

源码成熟度不错,但这是 2026-08-22 新仓库,风险主要在生态稳定性和运行面复杂度。下面这些不是否定项目,而是使用前需要明确的工程边界。

新项目版本变化快
最近 commit 是 2026-08-26,公开 API 与 profile schema 可能继续变化。依赖 sources/frontieragent/workflows/agent_team/profile.py:27 这类 alias 兼容层时要锁版本。
权限模型仍需外部制度
代码能在 sources/frontieragent/apodex/observers.py:498 请求确认,但团队仍要规定哪些命令能 remember、哪些必须人工复核。
并行不是免费
SpawnGuard 和 session cap 降低失控风险,但多 agent 会放大 token、网络和验证成本;sources/frontieragent/plugins/tools/collect_reports.py:181 还需要按 wall-clock 剩余预算截断等待。
评测资产仍要看任务域
benchmarks 存在不代表你的私有工作流已验证。应把 sources/frontieragent/apodex/tests/test_profiles.py:112 这种 provider/runtime 测试扩展到自己的模型和沙箱。
诚实记录 — 这部分列的是我在源码里看到、但作者没有明确文档化的限制。读者请按需取舍。