<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>状态机 on Renxin's Blog</title><link>https://zh.renxinblog.cn/tags/%E7%8A%B6%E6%80%81%E6%9C%BA/</link><description>Recent content in 状态机 on Renxin's Blog</description><generator>Hugo -- gohugo.io</generator><language>zh</language><lastBuildDate>Sat, 04 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://zh.renxinblog.cn/tags/%E7%8A%B6%E6%80%81%E6%9C%BA/index.xml" rel="self" type="application/rss+xml"/><item><title>第十一话｜Agent 为什么会停下来：从模型信号到 Runtime 状态迁移</title><link>https://zh.renxinblog.cn/post/c11-suspend-protocol/</link><pubDate>Sat, 04 Jul 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/c11-suspend-protocol/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/c11-suspend-protocol-cover.png" alt="Featured image of post 第十一话｜Agent 为什么会停下来：从模型信号到 Runtime 状态迁移" /&gt;&lt;p&gt;给一个 Agent 下达这样的任务：&lt;strong&gt;根据给定主题和本地资料，整理一份有依据的技术文章创作纲领，然后停下来等用户确认；确认之前，不生成正文草稿。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 Codex、WorkBuddy 这类产品里，这个任务的表现可以直接观察到：它检索、读资料、写出一份纲领，然后&lt;strong&gt;停下&lt;/strong&gt;，把纲领和一句「是否继续」摆在你面前；你回复之后，它接着往下做。&lt;/p&gt;
&lt;p&gt;停这件事看起来理所当然，其实需要解释。模型的输出是一段文本或一段 JSON。它在里面写「请确认」四个字，不会让 Python 的 &lt;code&gt;while&lt;/code&gt; 循环条件变成 False，也不会让 Java 的线程进入等待。语言表达能力，和程序控制流，是两码事。&lt;/p&gt;
&lt;p&gt;上一话把问题停在「下一步到底由谁拍板」。这个问题的答案里有一类决定和工具动作不同：工具动作有对应的执行路径，而「停下来等用户」没有对应动作可执行——它要求的是一次&lt;strong&gt;状态变化&lt;/strong&gt;。模型已经说了「请确认」，程序为什么不像往常那样继续循环？&lt;/p&gt;
&lt;p&gt;整篇只回答这一件事：&lt;strong&gt;模型只产生候选信号，Runtime 才拥有状态迁移权。&lt;/strong&gt; 前面所有的机制、对照、证据，最后都汇到这句话上。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="一请确认不是指令"&gt;一、「请确认」不是指令
&lt;/h2&gt;&lt;p&gt;先回答第一个问题：模型说出「请确认」，为什么本身不能停掉任何东西？&lt;/p&gt;
&lt;p&gt;先看两个最短的版本。它们跑同一个模型、同一个任务，唯一的区别是循环里有没有一处处理等待信号。&lt;/p&gt;
&lt;p&gt;缺陷版：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 没有处理 REQUEST_USER_INPUT：它只是一行被打印出来的输出&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;正确版：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;RunStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RUNNING&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;transition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;transition&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;RunStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WAITING_FOR_USER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;transition&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;差别只有一个分支。缺陷版把决定当数据打印出来，控制流继续；正确版把决定交给 Runtime，由 Runtime 迁移状态、再由调度循环按状态退出。&lt;/p&gt;
&lt;p&gt;把这两个版本放回真实 demo 上，用同一批离线夹具各跑三种控制器，输出是这样的（数字取自本仓库 &lt;code&gt;traces/offline-python-normal.log&lt;/code&gt; 与 &lt;code&gt;traces/offline-python-naive.log&lt;/code&gt;，Java 侧逐字段一致）。为了不在一开始就淹没在代号里，先只看两列最要紧的：最终状态，和「草稿到底写没写出来」。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;模式&lt;/th&gt;
					&lt;th&gt;最终状态&lt;/th&gt;
					&lt;th&gt;草稿写出&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;缺陷版 react&lt;/td&gt;
					&lt;td&gt;还在跑（RUNNING）&lt;/td&gt;
					&lt;td&gt;否&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;缺陷版 plan_and_execute&lt;/td&gt;
					&lt;td&gt;报完成（COMPLETED）&lt;/td&gt;
					&lt;td&gt;否&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;缺陷版 workflow&lt;/td&gt;
					&lt;td&gt;报完成（COMPLETED）&lt;/td&gt;
					&lt;td&gt;否&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;正确版 react&lt;/td&gt;
					&lt;td&gt;等用户（WAITING_FOR_USER）&lt;/td&gt;
					&lt;td&gt;否&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;正确版 plan_and_execute&lt;/td&gt;
					&lt;td&gt;等用户（WAITING_FOR_USER）&lt;/td&gt;
					&lt;td&gt;否&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;正确版 workflow&lt;/td&gt;
					&lt;td&gt;等用户（WAITING_FOR_USER）&lt;/td&gt;
					&lt;td&gt;否&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;一张表就能看懂差别：正确版三种模式都停在「等用户」，缺陷版没有一行停对——要么还在跑，要么更糟，报了个「完成」却没写出任何东西。&lt;/p&gt;
&lt;p&gt;缺陷版那两种走法，各自说不上停：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;react&lt;/code&gt; 把预算耗光了（一路反复读资料读到 8 步用尽），状态停在 &lt;code&gt;RUNNING&lt;/code&gt;，但日志里却有一个「说过要等」的标记。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;plan_and_execute&lt;/code&gt; 越过了等待点，一路走到 &lt;code&gt;write_draft&lt;/code&gt;（写草稿），被准入检查拦下，然后以 &lt;code&gt;COMPLETED&lt;/code&gt; 收场——一份&lt;strong&gt;没有被写出来的草稿&lt;/strong&gt;配一个完成状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第二种走法值得单独看一眼：它既没有写任何东西，也没有停下来，最后还报了个完成。这就是「没有写入不等于已经暂停」的最小复现。缺陷版里那个「说过要等」的标记只是一个数据记录，它记录「模型说过要等」，不记录「循环已经退出」。&lt;/p&gt;
&lt;p&gt;结论落在这一步：语言表达本身不能暂停程序。能被处理的，是被 Runtime 识别出来的结构化信号。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="二停下来的那一刻长什么样"&gt;二、停下来的那一刻，长什么样
&lt;/h2&gt;&lt;p&gt;上一节证明了「信号不能自动停程序」。这一节反过来问：当程序&lt;strong&gt;真的&lt;/strong&gt;停下来时，我们凭什么说它停了？换句话说，什么是「停」的判据。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;status == WAITING_FOR_USER&lt;/code&gt;（状态标记为等用户）本身不构成证明。它只说明 Runtime 写了一个值；写值的那一刻，和「写完之后没有新调用」，是两件事。要证明停，得看等待之后有没有新的调用发生。&lt;/p&gt;
&lt;p&gt;判据写成函数是这样：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_run_suspended&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ModeResult&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;final_state&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;model_delta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calls_after_wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;RunStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WAITING_FOR_USER&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;model_delta&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;tool_delta&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;四个条件同时成立才算停：状态是 &lt;code&gt;WAITING_FOR_USER&lt;/code&gt;、留了 pending request（待处理的请求）、等待之后没有新的模型调用、没有新的工具尝试。缺陷版会在这条判据上失败——这正是测试发现它的方式，而不是只证明正确版能通过。&lt;/p&gt;
&lt;p&gt;「等待之后」要算得出来，得在迁移的同一刻做快照：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;snapshot_wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model_calls_at_wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model_calls&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_attempts_at_wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_attempts&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这不是可有可无的一行。如果只在循环结束时同步计数，等待后的增量就会被算成全部调用——判据看起来还在测，其实已经失效。这个缺陷在重构时出现过一次，被测试逮住。&lt;/p&gt;
&lt;p&gt;判据要落到一组断言上才算验收。这次 Python 与 Java 各 20 项测试，覆盖的是同一批语义：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;验收断言&lt;/th&gt;
					&lt;th&gt;落在哪里&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;等待后模型调用数不再增加&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;client.calls&lt;/code&gt; 在迁移之后不再变化&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;等待后业务工具尝试数不再增加&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;tool_attempts&lt;/code&gt; 在迁移之后不再变化&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;受保护写入没有被尝试&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;write_draft&lt;/code&gt; 从未成功落库&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;pending request 完整&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;request_id&lt;/code&gt; / &lt;code&gt;question&lt;/code&gt; / &lt;code&gt;state_before&lt;/code&gt; 都在&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;方法已返回调用方&lt;/td&gt;
					&lt;td&gt;调度循环以 &lt;code&gt;run_exited&lt;/code&gt; 结束，不是预算耗尽&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;非法等待信号被拒&lt;/td&gt;
					&lt;td&gt;缺失状态下 &lt;code&gt;REQUEST_USER_INPUT&lt;/code&gt; 不产生迁移&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;一次等待在 Trace 里留下五类事件，按顺序出现：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;signal_received ← 信号由谁产生（signal_source）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;runtime_validated ← Runtime 是否接受（runtime_decision）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;state_transition ← state_before → state_after
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pending_recorded ← pending_request_id
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;run_exited ← Scheduler 退出当前 Run
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;三个信号来源产生的都是这同一个序列，类型和顺序一致。真正回答「停了吗」的是最后一个事件：&lt;code&gt;run_exited&lt;/code&gt; 而非 &lt;code&gt;budget_exhausted&lt;/code&gt;。前者是调度器主动退出，后者是把预算用完了。&lt;/p&gt;
&lt;p&gt;到这里，我们已经知道「停」长什么样、怎么判。但还没说：&lt;strong&gt;谁有资格做那个把状态改掉的动作？&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="三agent-内部谁有资格让-run-停"&gt;三、Agent 内部，谁有资格让 Run 停
&lt;/h2&gt;&lt;p&gt;这一节回答的是全文最关键的一处分工：Agent 的模型、Runtime、调度器、界面四层，各自能做什么、不能做什么。&lt;/p&gt;
&lt;p&gt;把角色摆到后端工程师熟悉的位置上，整条链只有四层：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;层&lt;/th&gt;
					&lt;th&gt;负责什么&lt;/th&gt;
					&lt;th&gt;不负责什么&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;模型&lt;/td&gt;
					&lt;td&gt;根据当前上下文提出动作、结束、或请求用户输入&lt;/td&gt;
					&lt;td&gt;不改变程序状态、不停止循环、不执行任何动作&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Runtime&lt;/td&gt;
					&lt;td&gt;解析模型输出，校验这条信号在当前状态是否合法，决定状态迁移&lt;/td&gt;
					&lt;td&gt;不生成内容、不选业务动作&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Scheduler / Agent Loop&lt;/td&gt;
					&lt;td&gt;按 Runtime 给出的状态决定继续循环还是退出当前 Run&lt;/td&gt;
					&lt;td&gt;不解释模型语义、不做合法性判断&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;UI / Continuation&lt;/td&gt;
					&lt;td&gt;展示问题、接收用户输入、创建下一次执行&lt;/td&gt;
					&lt;td&gt;不修改状态、不推断用户想要什么&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;换成后端里已有的说法：模型的输出是一条&lt;strong&gt;消息&lt;/strong&gt;（或一个领域事件）；Runtime 是&lt;strong&gt;消费者 + 状态机&lt;/strong&gt;；Scheduler 是&lt;strong&gt;任务调度器&lt;/strong&gt;；UI 是&lt;strong&gt;等待外部命令的入口&lt;/strong&gt;。消息本身不会被执行，它要被消费、被解释、被判断，才产生后果。&lt;/p&gt;
&lt;p&gt;链上只有一个位置能写 &lt;code&gt;status&lt;/code&gt;，就是 Runtime。其余三层都可以提请求，但提请求和改状态是两回事。&lt;/p&gt;
&lt;p&gt;这不是某一家的实现偏好，Anthropic 把它写进了 Messages API 的接口契约：每个响应都带一个 &lt;code&gt;stop_reason&lt;/code&gt; 字段，说明模型为什么结束生成；调用方检查这个字段，再决定是原样使用、继续对话、重试，还是回退到别的模型。字段的取值里没有任何一个是「程序已暂停」——&lt;code&gt;tool_use&lt;/code&gt; 的含义是「我在调工具，你去执行」，&lt;code&gt;end_turn&lt;/code&gt; 的含义是「我说完了」。模型交出的是一个原因码，怎么处理由外层决定。&lt;/p&gt;
&lt;p&gt;同一份契约在 &lt;code&gt;pause_turn&lt;/code&gt; 上表现得更清楚：服务端工具循环达到迭代上限时，响应会带 &lt;code&gt;pause_turn&lt;/code&gt; 返回。这不是进程被挂起，而是调用方拿到一段没处理完的响应，需要把它原样发回去让模型接着跑。&lt;/p&gt;
&lt;p&gt;把四层串起来，数据流是这样：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;模型生成候选决定
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Runtime 解析决定类型
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Runtime 检查当前状态是否允许这条决定
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;状态从 RUNNING 迁移到 WAITING_FOR_USER
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;记录 pending request
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Scheduler 看到 WAITING，退出当前 Run
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;UI 展示问题
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;用户提交下一条输入
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;新的 Run / continuation 根据关联标识继续
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;%%{init: {'theme':'neutral'}}%%
flowchart TB
 M["模型&lt;br/&gt;只提出决定"] --&gt;|"决策"| R["Runtime&lt;br/&gt;解析 → 校验 → 迁移"]
 R --&gt;|"CONTINUE"| M
 R --&gt;|"WAITING_FOR_USER"| S["Scheduler&lt;br/&gt;退出当前 Run"]
 S --&gt; U["UI / Continuation&lt;br/&gt;展示问题、接收输入"]
 U -.-&gt;|"下一次 Run"| M&lt;/pre&gt;&lt;p&gt;最后一步「根据关联标识继续」在这里只做到单进程的程度，跨进程的部分留到下一话。&lt;/p&gt;
&lt;h3 id="信号要能被识别得先有一个共同的格式"&gt;信号要能被识别，得先有一个共同的格式
&lt;/h3&gt;&lt;p&gt;三种来源共同遵守的格式，是四种决定类型：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DecisionType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;EXECUTE_TOOL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;EXECUTE_TOOL&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;REQUEST_USER_INPUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;REQUEST_USER_INPUT&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;FINISH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;FINISH&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;FAIL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;FAIL&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;模型、Planner、Workflow 产出的都是同一个结构，而不是执行结果：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ControlDecision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DecisionType&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;字段的可选性是刻意的。&lt;code&gt;REQUEST_USER_INPUT&lt;/code&gt; 用 &lt;code&gt;question&lt;/code&gt;，&lt;code&gt;EXECUTE_TOOL&lt;/code&gt; 用 &lt;code&gt;action&lt;/code&gt; 和 &lt;code&gt;args&lt;/code&gt;；某一类里字段缺失是正常输入，不应该在解析阶段就崩，校验属于 Runtime 的职责。&lt;/p&gt;
&lt;p&gt;调度循环的形状因此变得很平：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;RunStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RUNNING&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;transition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;transition&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;SUSPEND&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;COMPLETE&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;FAIL&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;break&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;controller.decide()&lt;/code&gt; 只提出决定；&lt;code&gt;runtime.handle()&lt;/code&gt; 才拥有状态迁移权。这句分工是后面所有代码的前提，它把「谁让 Run 停」从模型和模式里移出来，交给了一个有名字的组件。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;runtime.handle()&lt;/code&gt; 内部按五个动作走：&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;%%{init: {'theme':'neutral'}}%%
flowchart TB
 A["原始输出"] --&gt; B["1-2 解析 + 类型校验&lt;br/&gt;解析失败即明确失败"]
 B --&gt; C["3 状态前置条件校验&lt;br/&gt;当前状态是否允许这条决定"]
 C --&gt;|"不满足"| D["拒绝并记账"]
 C --&gt;|"满足"| E["4 迁移状态 + 记录 pending request"]
 E --&gt; F["5 返回 transition&lt;br/&gt;Scheduler 据此决定是否退出"]&lt;/pre&gt;&lt;p&gt;第 3 步是新增的一层，也是这里唯一需要做取舍的地方：模型请求等待时，Runtime 要不要校验它此刻有没有等待的对象？如果只做类型校验，模型可以在任何时刻宣称「我要等用户」，而那时根本不存在被等待的东西。所以 &lt;code&gt;REQUEST_USER_INPUT&lt;/code&gt; 在纲领生成之前一律拒绝：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;DecisionType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REQUEST_USER_INPUT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;brief_created&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;wait_signal_without_pending_object&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;wait_signal_without_question&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;非法的等待信号不会悄悄过去：&lt;code&gt;rejected_signals&lt;/code&gt;（被拒信号数）加一，Run 转成明确失败。合法的等待信号则走完整的迁移，留下 pending request：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pending_seq&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PendingRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;approval_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pending_seq&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;03d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;state_before&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;state_before&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RunStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WAITING_FOR_USER&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;snapshot_wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;state_before&lt;/code&gt; 记下迁移前的状态，&lt;code&gt;question&lt;/code&gt; 记下要问用户什么，&lt;code&gt;request_id&lt;/code&gt; 是用户回来时用来关联的标识。三者一起构成「这一次等待是谁、为了什么、从哪个状态发起」。&lt;/p&gt;
&lt;p&gt;这也正好对上 OpenAI Agents SDK 的用法：那里批准一项工具调用的方式是围绕 &lt;code&gt;interruptions&lt;/code&gt; 展开的——运行中断后，结果里带回一个待处理数组和一份可恢复的状态，批准就是在这个状态上做的决定。两种写法用词不同，位置一样：&lt;strong&gt;改状态的那一步在框架（Runtime）里，不在模型输出里。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="四等待信号从哪来"&gt;四、等待信号从哪来
&lt;/h2&gt;&lt;p&gt;现在回到上一节开头埋下的问题：模型不是唯一会提「要停」的角色。同一个 Runtime 入口，等待信号可以来自三个地方。三种控制器共用同一套工具、同一个 ActionGateway、同一个 Runtime，差异只留在「谁产生信号」这一处。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ReAct&lt;/strong&gt;：模型每次看到工具结果后重新提出一条决定。等待信号由模型自己在上下文里提出，比如输出 &lt;code&gt;{&amp;quot;type&amp;quot;: &amp;quot;REQUEST_USER_INPUT&amp;quot;, &amp;quot;question&amp;quot;: &amp;quot;纲领已生成，是否继续写正文？&amp;quot;}&lt;/code&gt;。Runtime 认出来就停。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;计划步&lt;/strong&gt;：Planner 一次生成整份计划，其中一步是等待。Executor 执行到那一步，把它原样交给 Runtime。计划可以声明等待点，但计划文本本身没有暂停能力。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workflow 节点&lt;/strong&gt;：节点序列写在代码里，等待是其中固定的一站。节点内部的内容仍然由模型生成，代码固定的是「走到哪个节点」。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;三种来源最后汇入同一个入口：&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;%%{init: {'theme':'neutral'}}%%
flowchart LR
 R["ReAct&lt;br/&gt;模型提出"] --&gt; RT["Runtime.handle()&lt;br/&gt;接收 ControlDecision"]
 P["计划步&lt;br/&gt;Planner 写入计划"] --&gt; RT
 W["Workflow 节点&lt;br/&gt;代码固定位置"] --&gt; RT
 RT --&gt; ST["RunState&lt;br/&gt;RUNNING → WAITING_FOR_USER"]
 ST --&gt; SC["Scheduler&lt;br/&gt;退出当前 Run"]&lt;/pre&gt;&lt;p&gt;这就是三种模式在这篇文章里真正的位置：它们是「信号来源」，不是「三种对等的 Agent 设计」。谁先提不重要，重要的是&lt;strong&gt;提信号和停程序是两件事&lt;/strong&gt;——无论信号来自模型、计划还是代码，改状态的那一步都在 Runtime。&lt;/p&gt;
&lt;p&gt;三个来源的模型调用次数本来就不同，这一点在输出里也如实保留：正确版 &lt;code&gt;react&lt;/code&gt; 是 5 次，&lt;code&gt;plan_and_execute&lt;/code&gt; 和 &lt;code&gt;workflow&lt;/code&gt; 各 1 次。决策粒度不同，调用次数就不该被凑成一个数。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;两条补充，都是旧版说错、这次改对的，不影响主线：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;关于「模型能不能主动请求确认」，这次跑了一组真实实验：&lt;code&gt;deepseek-v4-flash&lt;/code&gt;，6 次，日志在 &lt;code&gt;traces/live-run-1..6.log&lt;/code&gt;。结果是&lt;strong&gt;这 6 次里没有一次&lt;/strong&gt;产出可被 Runtime 解释的等待决定——3 次把 8 步预算耗在反复读同一份资料上，另外 3 次的输出无法解析（模型把决定多包了一层 &lt;code&gt;{&amp;quot;type&amp;quot;:&amp;quot;json_object&amp;quot;,&amp;quot;content&amp;quot;:{...}}&lt;/code&gt; 信封，Runtime 只认顶层的 &lt;code&gt;type&lt;/code&gt;，认不出就判失败）。这段证据的强度有明确边界：它只说明在指定模型、提示词、工具协议和输入下模型没有产生预期结构，不能推广成「模型不会请求确认」。同一个服务端在 &lt;code&gt;temperature=0&lt;/code&gt; 下输出并不可复现：这 6 次里预算耗尽 3 次、解析失败 3 次，两次分批运行的分布还不同。&lt;/p&gt;
&lt;p&gt;关于 ReAct 的旧结论也要改：之前把「绕圈」归因为「每一步只拿到当前观察，看不到任务整体」，这与代码不符——每次请求传入的是完整的观察累积历史，不是最新一条。当前能写的只有：在本次模型、提示词、动作协议和 8 步预算下，模型连续重复读取了同一份资料；Trace 证明了重复行为，不足以把原因唯一归结为 ReAct 模式。&lt;/p&gt;
&lt;p&gt;另外两处旧版的不实也一并纠正：Planner 的提示词曾硬编码「先读 material_1，再读 material_2」，而 ReAct 必须自己从工具结果里发现顺序，那不是对等的比较——现在三个控制器拿到同一份初始清单，只知道有哪些资料、不知道先读哪一份；Workflow 曾有一个假分支——代码无条件读取第二份资料，却在 Trace 里标成「读到线索之后触发的分支」，现在第 3 个节点做真实判断，没有线索就不读第二份，&lt;code&gt;branch_taken&lt;/code&gt; 如实记为 &lt;code&gt;false&lt;/code&gt;。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;三种实现可以组合，它们不在同一个对立面上：Workflow 固定阶段，ReAct 在阶段内部按线索选资料，Planner 负责跨子任务的覆盖。组合之后，「停」这件事仍然只有 Runtime 一处负责。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="五为什么不能只用一个布尔值"&gt;五、为什么不能只用一个布尔值
&lt;/h2&gt;&lt;p&gt;前面一直在用 &lt;code&gt;status&lt;/code&gt; 这个枚举。这一节讲为什么它必须是枚举，不能是 &lt;code&gt;waiting=true&lt;/code&gt; 这样的布尔。&lt;/p&gt;
&lt;p&gt;状态本身是枚举：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RunStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;RUNNING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;RUNNING&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;WAITING_FOR_USER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;WAITING_FOR_USER&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;COMPLETED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;COMPLETED&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;FAILED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;FAILED&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;用布尔 &lt;code&gt;waiting=true&lt;/code&gt; 表示等待，会丢掉两件需要区分的事：一是「等待」和「完成」「失败」是并列的终态关系，布尔表达不了；二是标记为真和调度退出是两步，布尔把两步压成一步，就再也分不出「标记了但还在跑」这种状态。缺陷版 &lt;code&gt;plan_and_execute&lt;/code&gt; 那一行正是这个压缩的后果：它有个「说过要等」的标记，状态却一路跑到了「完成」。&lt;/p&gt;
&lt;p&gt;回到那张表补全字段，现在这些代号已经都有中文含义了：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;模式&lt;/th&gt;
					&lt;th&gt;terminal_reason&lt;br/&gt;（结束原因）&lt;/th&gt;
					&lt;th&gt;status&lt;br/&gt;（状态）&lt;/th&gt;
					&lt;th&gt;waiting_marker&lt;br/&gt;（说过要等）&lt;/th&gt;
					&lt;th&gt;model_calls&lt;br/&gt;（模型调用）&lt;/th&gt;
					&lt;th&gt;tool_attempts&lt;br/&gt;（工具尝试）&lt;/th&gt;
					&lt;th&gt;rejected_actions&lt;br/&gt;（被拦动作）&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;缺陷版 react&lt;/td&gt;
					&lt;td&gt;budget_exhausted&lt;/td&gt;
					&lt;td&gt;RUNNING&lt;/td&gt;
					&lt;td&gt;true&lt;/td&gt;
					&lt;td&gt;8&lt;/td&gt;
					&lt;td&gt;4&lt;/td&gt;
					&lt;td&gt;0&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;缺陷版 plan_and_execute&lt;/td&gt;
					&lt;td&gt;completed&lt;/td&gt;
					&lt;td&gt;COMPLETED&lt;/td&gt;
					&lt;td&gt;true&lt;/td&gt;
					&lt;td&gt;1&lt;/td&gt;
					&lt;td&gt;5&lt;/td&gt;
					&lt;td&gt;1&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;缺陷版 workflow&lt;/td&gt;
					&lt;td&gt;completed&lt;/td&gt;
					&lt;td&gt;COMPLETED&lt;/td&gt;
					&lt;td&gt;true&lt;/td&gt;
					&lt;td&gt;1&lt;/td&gt;
					&lt;td&gt;4&lt;/td&gt;
					&lt;td&gt;0&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;正确版 react&lt;/td&gt;
					&lt;td&gt;wait_suspended&lt;/td&gt;
					&lt;td&gt;WAITING_FOR_USER&lt;/td&gt;
					&lt;td&gt;false&lt;/td&gt;
					&lt;td&gt;5&lt;/td&gt;
					&lt;td&gt;4&lt;/td&gt;
					&lt;td&gt;0&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;正确版 plan_and_execute&lt;/td&gt;
					&lt;td&gt;wait_suspended&lt;/td&gt;
					&lt;td&gt;WAITING_FOR_USER&lt;/td&gt;
					&lt;td&gt;false&lt;/td&gt;
					&lt;td&gt;1&lt;/td&gt;
					&lt;td&gt;4&lt;/td&gt;
					&lt;td&gt;0&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;正确版 workflow&lt;/td&gt;
					&lt;td&gt;wait_suspended&lt;/td&gt;
					&lt;td&gt;WAITING_FOR_USER&lt;/td&gt;
					&lt;td&gt;false&lt;/td&gt;
					&lt;td&gt;1&lt;/td&gt;
					&lt;td&gt;4&lt;/td&gt;
					&lt;td&gt;0&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;正确版三行的其余字段完全相同：&lt;code&gt;pending_request_id=approval_001&lt;/code&gt;、&lt;code&gt;draft_written=false&lt;/code&gt;、&lt;code&gt;approval_granted=false&lt;/code&gt;、&lt;code&gt;model_calls_after_wait=0&lt;/code&gt;、&lt;code&gt;tool_calls_after_wait=0&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;waiting_marker=true&lt;/code&gt; 在那三行缺陷版里都是一个数据标记，它记录「模型说过要等」，不记录「循环已经退出」——正是布尔值把这两件事压成一个所付出的代价。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="六和动作能不能执行是两回事"&gt;六、和「动作能不能执行」是两回事
&lt;/h2&gt;&lt;p&gt;最容易和「暂停」混起来的机制是动作准入。这一节把两个问题的边界切开。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;&lt;/th&gt;
					&lt;th&gt;Action Gateway&lt;/th&gt;
					&lt;th&gt;Runtime&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;回答的问题&lt;/td&gt;
					&lt;td&gt;这个动作能不能执行&lt;/td&gt;
					&lt;td&gt;当前 Run 要不要继续&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;检查时机&lt;/td&gt;
					&lt;td&gt;副作用发生之前&lt;/td&gt;
					&lt;td&gt;每条决定之后&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;拒绝的后果&lt;/td&gt;
					&lt;td&gt;这个动作不执行，Run 可以继续&lt;/td&gt;
					&lt;td&gt;状态迁移，调度可能退出&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;拦住之后&lt;/td&gt;
					&lt;td&gt;一次动作被挡&lt;/td&gt;
					&lt;td&gt;与「已暂停」无关&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;受保护写入的检查就在写入之前：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_write_draft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RunState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;approval_granted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;blocked_by_action_gateway&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;draft_written&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save_draft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;draft_1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;draft_written&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;未获批直接返回，不碰 store。这条检查把「写不进去」办到了，但它对「Run 停不停」一句话也没说。缺陷版 &lt;code&gt;plan_and_execute&lt;/code&gt; 那一行就是证据：&lt;code&gt;rejected_actions=1&lt;/code&gt;，写入被拦住了，循环继续跑到 &lt;code&gt;completed&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;Codex 的官方文档把这两种控制分开，用词几乎一样：沙盒模式定义的是「模型生成的命令在技术上可以做哪些操作」，审批策略定义的是「Codex 在执行某项操作之前必须何时停下来询问用户」。文档里那句话可以直接搬过来——沙盒定义技术边界，审批策略决定越界前何时必须停止并询问用户。前者对应 Gateway，后者才对应 Run 的继续与停止。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="七这些产品其实都这么做"&gt;七、这些产品，其实都这么做
&lt;/h2&gt;&lt;p&gt;前面把机制讲完了，这一节把视角拉远：这些机制不是我们自说自话，几个主流框架的文档描述的是同一件事。&lt;/p&gt;
&lt;p&gt;产品上的现象是清楚的：这些 Agent 会结束当前回合并把问题展示给用户，用户输入之后可以开启后续执行。这说明它们存在某种&lt;strong&gt;回合终止与 continuation 机制&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这一段能写到什么强度，取决于有没有官方来源。Codex 是有文档的：审批策略是运行配置，取 &lt;code&gt;on-request&lt;/code&gt; 表示默认在沙盒内工作、需要越界时询问；&lt;code&gt;approvals_reviewer&lt;/code&gt; 的默认值是 &lt;code&gt;&amp;quot;user&amp;quot;&lt;/code&gt;，即审批请求路由到用户，也可以设为 &lt;code&gt;auto_review&lt;/code&gt; 由审查智能体处理。这说明「停下来问谁」是运行时配置的一部分，而不是模型自身的能力开关。&lt;/p&gt;
&lt;p&gt;OpenAI Agents SDK 的文档把暂停这一步写得最直白：当一次工具调用需要审批时，&lt;strong&gt;SDK 暂停这次运行&lt;/strong&gt;，返回待处理项，之后可以从同一个 &lt;code&gt;RunState&lt;/code&gt; 恢复。暂停的执行者是 SDK，不是模型——分工和前面一致，只是它把 Runtime 的角色交给了框架。&lt;/p&gt;
&lt;p&gt;LangGraph 的 interrupt 走的是同一路线，并且把持久化的一环显式化：在节点里调用 &lt;code&gt;interrupt&lt;/code&gt; 之后，图执行在调用点挂起，状态由持久化层保存并无限期等待，恢复时用同一 &lt;code&gt;thread_id&lt;/code&gt; 重新调用图。文档同时说明，恢复时整个节点会从头重跑，&lt;code&gt;interrupt&lt;/code&gt; 之前的代码会再执行一次。这条细节很有用——它说明「挂着的那一步」和「恢复时怎么重放」是两个要分开设计的约束。&lt;/p&gt;
&lt;p&gt;从这些来源能推出来的，只到这里：&lt;strong&gt;被观察到的产品行为，与官方框架文档描述的「运行时暂停 + 携带状态恢复」是同一类机制。&lt;/strong&gt; 再往下不能推：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不能写它们内部一定用了某个类、某张表或某个具体状态机；&lt;/li&gt;
&lt;li&gt;不能写 WorkBuddy 与 Codex 的实现相同——WorkBuddy 这一段只有可观察行为，没有可引用的实现契约；&lt;/li&gt;
&lt;li&gt;不能把框架文档的接口承诺当成产品内部实现的证明。文档承诺的是「怎么用」，不是「里面怎么写的」。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这条边界不是保守，是证据强度决定的：官方文档能证明「这套机制存在且被这样描述」，证明不了「这个产品内部就是那样实现的」。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="八停只是前半场"&gt;八、停只是前半场
&lt;/h2&gt;&lt;p&gt;三种来源都在同一处收尾：纲领生成，Run 进入 &lt;code&gt;WAITING_FOR_USER&lt;/code&gt;，本次调用返回。Trace 里能看到状态、看到等待原因、看到哪些动作被拒绝，唯独看不到一件东西——&lt;strong&gt;这次调用退出之后，谁记得有一份纲领在等确认。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;单进程里 &lt;code&gt;return&lt;/code&gt; 退出的是当前这一次函数调用。&lt;code&gt;pending&lt;/code&gt;、&lt;code&gt;brief_id&lt;/code&gt;、已经走到哪一步，全在内存里，程序一退就没了。用户几分钟后回来，或者进程中途重启，系统凭什么找到对应的纲领、对应到哪一次等待、恢复到哪个进度？&lt;/p&gt;
&lt;p&gt;这一步不是加一行持久化就能收尾的，它牵出四个互相牵连的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;等待请求怎么存下来，重启之后还能按标识找回来；&lt;/li&gt;
&lt;li&gt;审批结果绑定到哪个对象——是这次 Run，还是被批准的那个具体动作；&lt;/li&gt;
&lt;li&gt;恢复之后怎样保证不重复产生副作用；&lt;/li&gt;
&lt;li&gt;重复审批、超时、用户直接取消，各自该走到哪个状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Anthropic 那句 &lt;code&gt;pause_turn&lt;/code&gt; 的语义在这里反过来读也成立：调用方必须把没处理完的内容原样发回去才能接着跑。也就是说，「恢复」不是自动发生的，它需要有人带着上下文重新发起。&lt;/p&gt;
&lt;p&gt;下一话接着讲：Run 停下来之后，怎样把状态存到进程之外，又怎样安全地接着跑。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="参考资料"&gt;参考资料
&lt;/h2&gt;&lt;p&gt;正文引用到的来源，按首次出现顺序排列：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Anthropic. Stop reasons and fallback. &lt;a class="link" href="https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons" target="_blank" rel="noopener"
 &gt;https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Anthropic. How tool use works. &lt;a class="link" href="https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/how-tool-use-works" target="_blank" rel="noopener"
 &gt;https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/how-tool-use-works&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;OpenAI. Guardrails and human review. &lt;a class="link" href="https://developers.openai.com/api/docs/guides/agents/guardrails-approvals" target="_blank" rel="noopener"
 &gt;https://developers.openai.com/api/docs/guides/agents/guardrails-approvals&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;OpenAI Agents SDK. Human-in-the-loop. &lt;a class="link" href="https://openai.github.io/openai-agents-js/guides/human-in-the-loop" target="_blank" rel="noopener"
 &gt;https://openai.github.io/openai-agents-js/guides/human-in-the-loop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;OpenAI. Codex: Agent approvals &amp;amp; security. &lt;a class="link" href="https://developers.openai.com/codex/agent-approvals-security" target="_blank" rel="noopener"
 &gt;https://developers.openai.com/codex/agent-approvals-security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;OpenAI. Codex: Sandbox. &lt;a class="link" href="https://developers.openai.com/codex/concepts/sandboxing" target="_blank" rel="noopener"
 &gt;https://developers.openai.com/codex/concepts/sandboxing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;LangChain. Plan-and-Execute Agents. &lt;a class="link" href="https://www.langchain.com/blog/planning-agents" target="_blank" rel="noopener"
 &gt;https://www.langchain.com/blog/planning-agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;LangGraph. Workflows and agents. &lt;a class="link" href="https://docs.langchain.com/oss/python/langgraph/workflows-agents" target="_blank" rel="noopener"
 &gt;https://docs.langchain.com/oss/python/langgraph/workflows-agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;LangGraph. Interrupts. &lt;a class="link" href="https://docs.langchain.com/oss/python/langgraph/interrupts" target="_blank" rel="noopener"
 &gt;https://docs.langchain.com/oss/python/langgraph/interrupts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Yao et al., 2022. ReAct: Synergizing Reasoning and Acting in Language Models. arXiv:2210.03629. &lt;a class="link" href="https://arxiv.org/abs/2210.03629" target="_blank" rel="noopener"
 &gt;https://arxiv.org/abs/2210.03629&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;配套 Python 与 Java 实现在 &lt;a class="link" href="https://github.com/renxin2024/GYA/tree/main/c11-suspend-protocol" target="_blank" rel="noopener"
 &gt;GYA&lt;/a&gt; 与 &lt;a class="link" href="https://github.com/renxin2024/GYA-Java/tree/main/c11-suspend-protocol" target="_blank" rel="noopener"
 &gt;GYA-Java&lt;/a&gt; 两个仓库的 &lt;code&gt;c11-suspend-protocol&lt;/code&gt; 目录，按 README 的命令跑。&lt;/p&gt;</description></item></channel></rss>