Terminus 2 手册

循环入口门控

stage-4.1Loop Entry Gates2 个函数

这个 Stage 在解决“这一轮能不能合法开始”的问题。它是每轮迭代的入口检查:先给这次尝试一个稳定的轮次编号,再确认终端会话(tmux session,可被程序远程控制的终端)还活着。它不负责观察、思考或行动,而是在真正进入后续 Stage 前,先把“可追踪”和“可继续”这两个前提立住。

  1. 先记轮次。

    这里会先更新内部轮次计数,并把它对外表现为 reg-n-episodes。这样做的意义是:哪怕这轮刚准备开始就发现 session 已结束,这次尝试也仍然有编号可查。对用户侧遥测来说,轮次是稳定的、连续的,不会因为检查失败而“少一轮”。

  2. 再查 session 还活不活。

    TmuxSession.is_session_alive()(检查当前 tmux 会话是否还存在)负责回答一个很基础的问题:后面那些 Stage 还有没有运行土壤。 如果会话已经没了,后面的观察、总结、行动都失去对象,继续跑只会制造噪音。

  3. 根据检查结果走两条明确路径。
    • alive = True:说明运行前提成立,调度继续,进入 stage-4.2 Proactive Summarize Probe
    • alive = False:记录 “Session has ended”,终止当前迭代,并阻断后续 Stage 调度。控制权直接返回循环外层;之后 run() 里的 finally 仍会照常收尾。
  4. 为什么这个检查必须放在这里。

    因为它属于“入口门禁”。太早放,还没有轮次上下文;太晚放,后面的 Stage 可能已经开始读终端、拼上下文、调 LLM(大语言模型),浪费成本。放在迭代最前面,能用最小代价挡住一整轮无效工作。

上游 stage-4 Iteration Loop ★ Core ★ 给的是“开始新一轮”的控制权。这个 Stage 产出两样东西:一个可追踪的轮次编号,以及一个明确的入口判定;前者会在后面进入上下文元数据,后者决定是继续调度到 [stage-4.2](stage-4.2.html),还是直接停止后续 Stage。

函数细节2

TmuxSession.is_session_alivetmux_session.py:518–524 ↗

tmux 会话存活性的布尔探针

stage 上下文: 该条目属于 stage-4.1 所需的会话存活检查能力,但这里仅覆盖 TmuxSession.is_session_alive 这一底层探针本身。函数体只负责发起一次 self.environment.exec(...) 调用并把其 return_code 映射为布尔值,不包含外层循环、日志或提前退出控制。作为本 stage 当前唯一已翻译单元,它提供的是兄弟逻辑可复用的原子检查结果。

这段代码在干什么

该方法异步执行一次命令探测:以 self._session_name 组装 tmux has-session -t ...,并以 self._user 身份通过 self.environment.exec(...) 发出。它读取 self.environmentself._session_nameself._user,等待得到 result 后返回 result.return_code == 0 的布尔值。方法本身不写入实例状态;唯一外部作用是委托环境层执行该命令。若 exec(...) 抛出异常,此处不捕获,异常将直接传播给调用方。

接口 · 参数 / IO

async def is_session_alive(self) -> bool

  • 参数: self: ? — 提供执行环境、会话名与用户身份的实例对象
  • 读状态: self.environment, self._session_name, self._user
  • 返回: 返回布尔值:当等待得到的 result.return_code == 0 时为 True,否则为 False;若 self.environment.exec(...) 抛出异常,则本方法不返回布尔值而是向上传播异常。
  • 副作用: 通过 self.environment.exec(...) 委托外部环境执行一次命令 tmux has-session -t <session>

执行流

  1. 构造传给 self.environment.exec(...) 的参数:command"tmux has-session -t {}".format(self._session_name)userself._user
  2. 异步等待 self.environment.exec(...) 完成,并将结果绑定到局部变量 result
  3. 读取 result.return_code,计算表达式 result.return_code == 0
  4. 返回上述比较结果作为方法的布尔返回值。

源码

    async def is_session_alive(self) -> bool:
        """Check if the tmux session is still alive."""
        result = await self.environment.exec(
            command="tmux has-session -t {}".format(self._session_name),
            user=self._user,
        )
        return result.return_code == 0

Non-obvious 设计决策

  • 该实现把命令结果压缩为 return_code == 0 的单一判定,而不检查 result 的其他字段;这是代码中直接可见的取舍,意味着调用方只得到成败布尔值而非更细粒度的执行信息。
  • 命令字符串通过 "tmux has-session -t {}".format(self._session_name) 直接由会话名插入,函数体未做额外转义或校验;这是当前实现选择的最直接参数化方式。
  • 方法不在本地直接启动进程,而是统一委托给 self.environment.exec(...);这样本函数只承担探测请求与结果映射,不承担环境执行细节。
  • await self.environment.exec(...) 没有 try/except 包裹,因此执行阶段出现的异常不会被吞掉或转换为 False,而是保持原样传播给调用方。

上下游关系

  • 调用方: 未在所给源码片段中展示;调用方未知,但应为需要布尔化会话探测结果的上层逻辑
  • 核心被调用: self.environment.exec
  • 配置/状态来源: self._session_name; self._user; self.environment
  • 结果去向: 直接作为本方法的布尔返回值交给调用方; 若 self.environment.exec(...) 失败,则异常直接传播到调用方
Terminus2._run_agent_loopterminus_2.py:1260–1281 ↗

代理循环入口的上下文与会话闸门

stage 上下文: 这段代码位于 _run_agent_loop 的循环入口,承担本 stage 所定义的两道前置闸门:先把对外可见的轮次计数写成 1-based 的 self._n_episodes,再检查 tmux 会话是否仍然存活。它只在正式 episode 逻辑开始前运行,因此既负责建立本次 run 的基础统计上下文,也负责执行 loop 内唯一的非完成态早退路径。与兄弟函数 TmuxSession.is_session_alive 的关系是:这里消费其布尔结果决定是否立即结束整个 agent loop,而不再进入后续 stage。

这段代码在干什么

该片段先验证运行所需的两个关键对象 self._contextself._session 均已就绪;若任一缺失,立即抛出 RuntimeError,拒绝在不完整状态下继续执行。随后它用 initial_prompt 初始化工作提示词,并把共享上下文中的输入/输出/cache token 计数与成本字段重置为本次 run 的初始值。进入 for episode in range(self._max_episodes) 后,每轮先将 self._n_episodes 更新为对外展示的 1-based 计数,再调用 await self._session.is_session_alive();若会话已结束,则仅记录 debug 日志并直接 return 退出整个循环函数。

接口 · 参数 / IO

(self, initial_prompt: str) -> None

  • 参数: self: Terminus2 — 持有运行上下文、tmux 会话、循环配置与统计状态的宿主对象; initial_prompt: str — 本次 agent loop 的初始工作提示词来源,被赋给局部变量 prompt
  • 读状态: self._context, self._session, self._max_episodes, self.logger
  • 返回: 返回 None。真正产出是:初始化 self._context 中的聚合统计字段、写入当前轮次 self._n_episodes,并在会话已死时提前终止 _run_agent_loop
  • 副作用: 写入局部变量 prompt = initial_prompt 作为后续循环工作的起点; 写入 self._context.n_input_tokens = 0; 写入 self._context.n_output_tokens = 0; 写入 self._context.n_cache_tokens = 0; 写入 self._context.cost_usd = None; 每轮写入 self._n_episodes = episode + 1; 异步调用 self._session.is_session_alive() 探测外部 tmux 会话状态; 在会话已结束时调用 self.logger.debug(...) 记录日志并提前返回

执行流

  1. 首先检查 self._context is None;若为真,抛出 RuntimeError("Agent context is not set. This should never happen."),阻止在缺失运行上下文时继续。
  2. 将入参 initial_prompt 赋给局部变量 prompt,作为后续 episode 流程的当前工作提示词起点。
  3. self._context 上的聚合统计字段统一重置:n_input_tokensn_output_tokensn_cache_tokens 归零,cost_usd 设为 None,为本次 run 建立干净的累计面板。
  4. 随后检查 self._session is None;若会话对象缺失,同样抛出 RuntimeError("Session is not set. This should never happen."),拒绝进入循环。
  5. 进入 for episode in range(self._max_episodes) 的 episode 主循环;每轮起始先执行 self._n_episodes = episode + 1,把内部从 0 开始的循环索引转换为对用户友好的 1-based 计数。
  6. 紧接着调用 await self._session.is_session_alive();若结果为假,则记录 debug 日志 "Session has ended, breaking out of agent loop",并直接 return 结束 _run_agent_loop,不再进入本轮后续阶段,也不再尝试下一轮。

源码

        if self._context is None:
            raise RuntimeError("Agent context is not set. This should never happen.")

        prompt = initial_prompt

        self._context.n_input_tokens = 0
        self._context.n_output_tokens = 0
        self._context.n_cache_tokens = 0
        self._context.cost_usd = None

        if self._session is None:
            raise RuntimeError("Session is not set. This should never happen.")

        # Step ID offset accounts for initial steps in the trajectory:
        # Step 1: system message (includes task instruction)
        # Steps 2+: agent episodes (starting from episode 0)

        for episode in range(self._max_episodes):
            self._n_episodes = episode + 1
            if not await self._session.is_session_alive():
                self.logger.debug("Session has ended, breaking out of agent loop")
                return

Non-obvious 设计决策

  • 这里对 self._contextself._session 使用显式 None 检查并抛出异常,而不是依赖后续属性访问自然报错;取舍点在于把失败原因固定为更清晰的运行前置条件违例,并用 "This should never happen." 标明这是内部不变量破坏而非常规业务分支。
  • 统计字段在进入 episode 循环前一次性重置到 self._context,而不是按轮重置;这表明这些 token/cost 指标是 run 级累计量,不是单轮局部量,否则 stage-5 将无法直接汇总整个试验过程。
  • self._n_episodes 写成 episode + 1 而不是直接暴露循环索引,体现了用户可见遥测与内部 0-based 控制变量分离的选择;若直接使用 episode,外部元数据会出现从 0 开始的轮次数,和注释所述的轨迹 step 编号语义也更难对齐。
  • 会话存活检查被放在每轮一开始,并在失败时直接 return 整个函数,而不是 break 后继续执行循环后逻辑;这样可保证这是 loop 内唯一的非完成态早退路径,并避免在底层 tmux 已终止时继续追加轨迹、发起 LLM 调用或执行其他后续阶段。

上下游关系

  • 调用方: Terminus2.run; Terminus2.run 内部在完成 stage-3 状态准备后进入 _run_agent_loop
  • 核心被调用: TmuxSession.is_session_alive; self.logger.debug
  • 配置/状态来源: self._context; self._session; self._max_episodes; initial_prompt
  • 结果去向: self._context.n_input_tokens; self._context.n_output_tokens; self._context.n_cache_tokens; self._context.cost_usd; self._n_episodes; 调用方 Terminus2.run 的后续 finally 清理路径
  • 同类 sibling: TmuxSession.is_session_alive:兄弟函数只负责通过 tmux has-session 返回会话是否存在;本段把该布尔值提升为 agent loop 的进入闸门,并在失败时执行本 stage 规定的唯一早退。
寄存器交互