Goal 与 Continuation

对应源码

主题源码
Goal metadata 解析与模型可见状态nanobot/session/goal_state.py
continuation 决策和保存边界nanobot/session/turn_continuation.py::maybe_continue_turn、prepare_save_boundary
Goal Toolsnanobot/agent/tools/long_task.py::LongTaskTool、CompleteGoalTool
Runner 的 Goal 继续机制nanobot/agent/runner.py::AgentRunner._try_drain_injections
Loop 接入点nanobot/agent/loop.py::AgentLoop._state_run、_state_save

学习目标

读完后,你应当能够:

  • 说明 sustained goal 保存在哪里;
  • 解释 long_task 和 complete_goal 的职责;
  • 说明达到 iteration budget 后如何内部续跑;
  • 区分 Goal、Cron 和 Subagent。

前置知识

  • 理解 Session metadata;
  • 理解 AgentRunner max iterations;
  • 理解 pending queue。

1. Goal 是 Session metadata

当前 key:

goal_state

旧 Session 可能使用兼容 key:

thread_goal

读取时接受旧 key,新的写入迁移到 goal_state。

Goal 典型字段包括:

  • objective;
  • status;
  • ui summary;
  • 创建/更新时间等工具写入信息。

Goal 不是单独数据库,也不是独立 Agent 进程。

2. 两个 Tool

long_task

主要作用:

  • 创建或更新 active goal;
  • 写入 Session metadata;
  • 清理 legacy key;
  • 保存 Session;
  • 发布 GoalStateChanged。

complete_goal

主要作用:

  • 将 active goal 标记完成;
  • 更新 metadata;
  • 保存;
  • 发布状态变化。

实现:

  • nanobot/agent/tools/long_task.py

3. Goal 如何持续进入上下文

goal_state_runtime_lines() 将 active objective 转成 Runtime Context 行:

Goal (active):
<objective>
Summary: ...

即使较早对话被 compaction,Goal 仍由 Session metadata 在每个 Turn 重新投影到上下文。

这比只把目标写在某条 user 消息中更稳定。

Goal metadata 仍需边界

objective 有长度上限,WebSocket snapshot 使用更短的展示上限。运行上下文和 UI 不是完整 metadata dump。

4. Sustained Goal 的 LLM wall timeout

runner_wall_llm_timeout_s() 对 sustained-goal Turn 返回 0.0,表示禁用 Runner 外层 asyncio.wait_for 的普通 wall timeout;其他 Turn 返回 None,使用环境变量默认策略。

这不代表:

  • Provider 永远不会超时;
  • stream idle timeout 被禁用;
  • 工具没有自己的 timeout;
  • Goal 可以无限运行。

它只调整 Runner 包裹单次 LLM 请求的 wall-clock cap。

5. 为什么需要 Continuation

单个 AgentRunner 仍受 max_iterations 约束。若 active goal 在本次预算内没有完成,直接向用户返回“达到上限”会打断长任务。

turn_continuation.maybe_continue_turn() 在满足条件时:

  1. 确认 stop reason 为 max_iterations;
  2. 确认 pending queue 可用;
  3. 确认 Goal 仍 active;
  4. 确认 continuation 轮数未达到上限;
  5. 创建内部 continuation metadata;
  6. 去掉人工生成的终止 assistant 消息;
  7. 抑制当前用户可见回复;
  8. 将新的内部 InboundMessage 放入 pending queue。
flowchart TD
    R["Runner: max_iterations"] --> A{"Goal active?"}
    A -->|否| FINAL["正常完成上限处理"]
    A -->|是| Q{"pending queue 可用<br/>且轮数未超限?"}
    Q -->|否| FINAL
    Q -->|是| I["生成 internal continuation"]
    I --> S["当前 slice 保存但不回复"]
    S --> N["下一 slice 继续同一 Session"]
    N --> R

6. Continuation 不是新用户消息

内部消息带有:

  • _internal_continuation
  • continuation kind;
  • 原始可见运行开始时间;
  • 跳过或控制 user 持久化的 metadata;
  • session_key_override。

保存边界会避免把内部 continuation prompt 当作普通用户输入重复写入历史。

7. 最大续跑轮数

当前内部常量:

_MAX_GOAL_CONTINUATION_ROUNDS = 12

它限制一次 sustained goal 在达到每个 Runner iteration budget 后可以自动续跑多少个 slice。

达到上限仍需要产生明确的用户可见结果,不能静默无限后台运行。

8. Goal、Cron、Subagent 的区别

机制核心形式是否后台是否持久化
GoalSession metadata否,本质仍是 Turn是
Continuationpending queue 内部消息当前 Session 内续跑间接
Cron持久化 schedule/job是是
Subagent后台 AgentRunner Task是任务状态主要在内存,结果回主 Session

观察任务

  1. 在隔离 Session 中触发 /goal 或 long_task;
  2. 查看 Session metadata 的 goal_state;
  3. 在 ContextBuilder 的 runtime lines 中找到 Goal;
  4. 观察 max iteration 时是否进入 continuation;
  5. 调用 complete_goal 后确认 metadata 和 runtime event 变化;
  6. 不人为降低安全限制来制造长任务。

源码与测试

  • nanobot/session/goal_state.py
  • nanobot/session/turn_continuation.py
  • nanobot/agent/tools/long_task.py
  • nanobot/agent/loop.py::_state_run
  • tests/agent/tools/test_long_task.py
  • tests/agent/test_runner_goal_continue.py
  • tests/agent/test_loop_goal_wall_timeout.py

自检

  1. Goal 为什么要放在 Session metadata,而不是只放在历史消息?
  2. sustained goal 禁用的是什么 timeout,哪些 timeout 仍存在?
  3. continuation 为什么使用 pending queue?
  4. 内部 continuation prompt 为什么不应作为普通 user 消息保存?
  5. Goal 与 Subagent 为什么不能互换理解?

下一步

阅读 Cron 与 Heartbeat。