Skip to content

[架构] Host 托管 ingestion 长事务等待,避免 Agent 轮询耗尽 max_iterations #142

Description

@noho

背景

当前 ingestion download/process 已经被 job 化,但 Agent 仍在同一个 turn 内反复调用 status 工具等待终态。典型 prompt:

下载 META 最新季报,给出摘要

实际链路容易变成:

start_financial_filing_download_job -> get_financial_filing_download_job_status -> ... -> max_iterations -> 基于阶段进度降级答复

这不是 max_iterations 太小,而是等待职责放错层:LLM 负责了机械轮询,Host 没有托管“外部长事务等待 + 终态唤醒”。

当前代码证据

  • dayu/fins/ingestion/job_manager.py 已说明 ingestion job 生命周期独立于 AsyncAgent,且 download/process 各自串行执行一个 job。这说明 job 化基础已存在。
  • dayu/fins/tools/ingestion_tools.py 的 start/status 描述仍明确要求“下一步只用状态工具轮询,直到终态”。
  • _build_next_step()queued/running/cancelling 时返回 next_step.action = "poll_status"tool_name = status_tool_namesuggested_wait_seconds = 5
  • DuplicateCallGuardpoll_until_terminal 的未终态结果显式放行重复调用,不触发重复工具保护。

结论:现状是“LLM 被允许并被提示机械轮询”,所以长下载耗尽 Agent iteration 是架构必然结果。

上游研究

Claude Code

项目/资料:

观察:

  • anthropics/claude-code 是公开仓库,但 README 和 plugins 目录显示它主要承载安装入口、插件、命令、agents、hooks 等外壳/扩展层,不等于完整 CLI 内核源码。
  • Claude Code 的 scheduled tasks 文档把长时间等待分成 /loop、Desktop tasks、Cloud tasks;还提到 Monitor tool 可以跑后台脚本并流式返回输出,避免反复重跑 prompt 轮询。
  • Ralph Wiggum plugin 展示了另一类 session 内 agent loop:通过 Stop hook 阻止 Claude 结束,并把同一个 prompt 重新喂回去,直到 completion promise 或 max-iterations。它适合测试/编码自我修复,不适合 Dayu ingestion 等待,因为它仍然让 Agent 留在循环里。

可借鉴点:

  • “等待一会再检查”应成为 runtime/session 能力,不应只靠 prompt 纪律。
  • Monitor/后台脚本模式说明:对于长 I/O,事件或日志流比 LLM 轮询更省 token。
  • Stop hook loop 可作为“防止误停”的参考,但不是 ingestion 的主路径。

Codex

项目/资料:

观察:

  • openai/codex 的 Unified Exec 明确把交互式/长命令作为 Host/runtime 管理的 process:
    • mod.rs 定义 ProcessStoreProcessEntryprocess_id、yield time 上限和后台 terminal timeout。
    • process_manager.rs 在 initial yield 之前持久化 live process,避免 turn 中断导致后台进程被释放。
    • async_watcher.rs 启动后台 watcher,持续读取输出,并在进程退出后发出统一的 end event。
  • Codex App 层面把多个 agent、长时程/background tasks、isolated worktrees、progress/decisions 展示给用户,形成控制平面。

可借鉴点:

  • 工具调用返回“不一定代表底层进程已完成”;返回时可以带 process_id/job_id,由 runtime 继续持有。
  • 输出/进度是 runtime event,不必全部进入 LLM 上下文。
  • UI 应展示 Host/runtime 进度;LLM 只在语义节点介入。

OpenClaw

项目/资料:

观察:

  • OpenClaw 明确说 task 是 activity ledger,不是 scheduler:cron/heartbeat 决定何时运行,task 记录发生了什么。
  • task 生命周期是 queued -> running -> terminal,终态包括 succeeded/failed/timed_out/cancelled/lost
  • 文档明确说 completion 是 push-driven:detached work 完成后直接通知 channel,或把 system event 放入 requester session,在下一次 heartbeat surfaced;通常不应写 status polling loops。
  • Cron 在 Gateway 进程里运行,任务定义持久化到 ~/.openclaw/cron/jobs.json,运行态在 jobs-state.json;所有 cron execution 都创建 background task record。

可借鉴点:

  • Dayu 需要 Host 侧 task/await ledger,而不是让 LLM 对每个 job 自己记忆。
  • 终态唤醒比轮询 prompt 更适合长事务。
  • lost/timed_out/cancelled 这类治理状态应该属于 Host,而不是 ingestion tool 自己临时拼返回文本。

Dayu 设计结论

目标不是删除 ingestion job 工具,而是把 status 轮询的主调用者从 LLM 挪到 Host。

建议引入 Host 托管的 awaitable tool/job 契约:

  1. 工具结果仍可返回 next_step.action = "poll_status",但同时提供机器可识别的 wait spec,例如:
{
  "awaitable": {
    "kind": "tool_job",
    "owner": "fins.ingestion",
    "job_id": "job_xxx",
    "status_tool_name": "get_financial_filing_download_job_status",
    "terminal_statuses": ["succeeded", "failed", "cancelled"],
    "suggested_wait_seconds": 5
  }
}
  1. AsyncAgent 不直接理解 ingestion;它只把工具结果事件交给 Host。
  2. Host/Executor 识别 wait spec 后:
    • 暂停当前 Agent run,不进入 force_answer。
    • 保留或更新 pending turn 真源。
    • 创建 Host-owned wait record/monitor。
    • 由 Host monitor 按 wait spec 调用 status 工具或对应 service 查询状态。
  3. job 进入终态后,Host resume pending turn,把终态 snapshot 注入上下文,让 Agent 继续执行原始目标。
  4. UI 从 Host wait record/job snapshot 展示进度,不把“下载中”作为 assistant 最终答案。

目标流程:

用户 prompt -> Agent 启动下载 -> Host 托管等待 -> ingestion job 终态 -> Host resume Agent -> Agent list_documents/read/summarize -> 最终摘要

分层约束

  • 不让 dayu.fins 反向依赖 Host。
  • 不让 Host 硬编码 ingestion 具体实现;Host 只识别通用 wait spec。
  • dayu.fins.storage 仍是财报文档存取唯一入口。
  • ingestion_tools 继续只暴露 start/status/cancel 业务工具;是否挂起/恢复由 Host 决定。
  • 不通过调大 max_iterations 修复;那只是把等待成本继续塞进 LLM loop。

建议落地阶段

阶段 1:契约与最小闭环

  • 定义通用 AwaitableToolJobSpec 类型。
  • ingestion start/status 返回 wait spec。
  • Host 在工具结果中识别 wait spec,先支持单 job 等待。
  • Host monitor 复用现有 status tool schema/tool executor 查询状态。
  • 终态后 resume pending turn,并注入一个结构化“等待结果”上下文。

阶段 2:治理

  • wait record 增加 timeout、cancel、lost、retryable failure。
  • UI 暴露 running wait 状态与取消入口。
  • Host 启动恢复时 reconcile 未终态 wait record。

阶段 3:泛化

  • 支持多个 awaitable job。
  • 支持 push/event-driven ingestion 事件,减少固定轮询。
  • 把 process/download 两类 ingestion job 统一到同一 wait contract。

验收标准

  • 对“下载 META 最新季报,给出摘要”,下载未完成时 Agent 不再持续调用 status 直到 max_iterations
  • 下载期间 UI 能看到 Host/job 进度。
  • 下载完成后,Agent 自动恢复并继续读取财报生成摘要。
  • 用户取消 run 时,Host 能取消或标记对应 wait/job,并保持 pending turn/run 状态一致。
  • pyright 无新增错误;新增/修改代码有对应测试。

风险与开放问题

  • wait spec 应放在工具业务返回里,还是放在 tool result metadata 里?倾向 metadata,避免污染 LLM 认知负担;但现有工具协议是否支持需要确认。
  • Host monitor 调用 status tool 还是直接调用 manager/service?倾向调用通用 tool executor 或抽象 status reader,避免 Host 依赖 dayu.fins 具体实现。
  • pending turn 的状态机是否需要新增 WAITING_EXTERNAL_JOB,还是复用现有 resumable/cancelled timeout 语义?需要结合 dayu/host 当前状态机设计。
  • 进程内 IngestionJobManager 的 job 是否需要持久化到 workspace,避免 Host 重启后只剩 pending turn 但 job manager 内存丢失。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions