背景
当前 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_name、suggested_wait_seconds = 5。
DuplicateCallGuard 对 poll_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 定义 ProcessStore、ProcessEntry、process_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 契约:
- 工具结果仍可返回
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
}
}
AsyncAgent 不直接理解 ingestion;它只把工具结果事件交给 Host。
- Host/Executor 识别 wait spec 后:
- 暂停当前 Agent run,不进入 force_answer。
- 保留或更新 pending turn 真源。
- 创建 Host-owned wait record/monitor。
- 由 Host monitor 按 wait spec 调用 status 工具或对应 service 查询状态。
- job 进入终态后,Host resume pending turn,把终态 snapshot 注入上下文,让 Agent 继续执行原始目标。
- 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 内存丢失。
背景
当前 ingestion download/process 已经被 job 化,但 Agent 仍在同一个 turn 内反复调用 status 工具等待终态。典型 prompt:
实际链路容易变成:
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_name、suggested_wait_seconds = 5。DuplicateCallGuard对poll_until_terminal的未终态结果显式放行重复调用,不触发重复工具保护。结论:现状是“LLM 被允许并被提示机械轮询”,所以长下载耗尽 Agent iteration 是架构必然结果。
上游研究
Claude Code
项目/资料:
观察:
anthropics/claude-code是公开仓库,但 README 和 plugins 目录显示它主要承载安装入口、插件、命令、agents、hooks 等外壳/扩展层,不等于完整 CLI 内核源码。/loop、Desktop tasks、Cloud tasks;还提到 Monitor tool 可以跑后台脚本并流式返回输出,避免反复重跑 prompt 轮询。可借鉴点:
Codex
项目/资料:
观察:
openai/codex的 Unified Exec 明确把交互式/长命令作为 Host/runtime 管理的 process:mod.rs定义ProcessStore、ProcessEntry、process_id、yield time 上限和后台 terminal timeout。process_manager.rs在 initial yield 之前持久化 live process,避免 turn 中断导致后台进程被释放。async_watcher.rs启动后台 watcher,持续读取输出,并在进程退出后发出统一的 end event。可借鉴点:
process_id/job_id,由 runtime 继续持有。OpenClaw
项目/资料:
观察:
queued -> running -> terminal,终态包括succeeded/failed/timed_out/cancelled/lost。~/.openclaw/cron/jobs.json,运行态在jobs-state.json;所有 cron execution 都创建 background task record。可借鉴点:
lost/timed_out/cancelled这类治理状态应该属于 Host,而不是 ingestion tool 自己临时拼返回文本。Dayu 设计结论
目标不是删除 ingestion job 工具,而是把 status 轮询的主调用者从 LLM 挪到 Host。
建议引入 Host 托管的 awaitable tool/job 契约:
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 } }AsyncAgent不直接理解 ingestion;它只把工具结果事件交给 Host。目标流程:
用户 prompt -> Agent 启动下载 -> Host 托管等待 -> ingestion job 终态 -> Host resume Agent -> Agent list_documents/read/summarize -> 最终摘要分层约束
dayu.fins反向依赖 Host。dayu.fins.storage仍是财报文档存取唯一入口。ingestion_tools继续只暴露start/status/cancel业务工具;是否挂起/恢复由 Host 决定。max_iterations修复;那只是把等待成本继续塞进 LLM loop。建议落地阶段
阶段 1:契约与最小闭环
AwaitableToolJobSpec类型。阶段 2:治理
阶段 3:泛化
验收标准
max_iterations。风险与开放问题
dayu.fins具体实现。WAITING_EXTERNAL_JOB,还是复用现有 resumable/cancelled timeout 语义?需要结合dayu/host当前状态机设计。IngestionJobManager的 job 是否需要持久化到 workspace,避免 Host 重启后只剩 pending turn 但 job manager 内存丢失。