当前没有活动 Turn
Core 创建新的 Turn 上下文,生成新的 Turn ID,并启动一个 RegularTask。
新工作开始,但尚未执行完成。
start_or_steer_turn上一站,App Server 已经把客户端请求翻译成 Core 能接收的内部对象。本课把“进入 Core 公开入口 → 通过 Session 队列 → 检查状态并选择分支 → 返回受理结果”这一整段叫作第三站。整段链路不调用模型、不执行工具;入口函数 start_or_steer_turn 本身不读取状态,私有函数 start_or_steer 组织三条主分支,而它调用的 Session::steer_input 才真正加锁读取活动 Turn 状态。
start_or_steer_turn 位于 Core 层,但它只是本段 Core 路由链的公开入口,不是状态决策点。
App Server / turn_start_inner(上游) → CodexThread::start_or_steer_turn(公开入口) → start_or_steer(选择主分支) → Session::steer_input(读取和检查活动状态)
下游有两条主路:新开 Turn 时进入 RegularTask;追加时进入当前活动 Turn 的输入队列。拒绝时不进入任何执行任务。
Started / Steered / NotSubmitted 三种受理结果。
| 你在界面里看到的东西 | 源码中的准确尺度 | 是否是一个新 Turn |
|---|---|---|
| 侧边栏里的整段长期对话 | 一个 Thread;里面可以长期追加很多个 Turn。 | 不是,它是 Turn 的容器。 |
| Thread 空闲时,你发送一次要求,Codex 一直工作到本次结束 | 一个 Turn;其中可以有用户输入、多个 Item、工具调用及多次模型采样。 | 是。 |
| 回答文字一个字、一小段一小段出现 | 同一 Agent Message 的多条 item/agentMessage/delta 通知。 | 不是。 |
| 当前 Turn 尚未结束时,你又追加输入 | 可能成为 Steering,追加进现有活动 Turn。 | 不是。 |
一个 Thread
├─ Turn A
│ ├─ turn/started
│ ├─ item/agentMessage/delta 「第 1 个流式片段」
│ ├─ item/agentMessage/delta 「第 2 个流式片段」
│ ├─ 工具调用、工具结果、更多 Item……
│ └─ turn/completed
└─ Turn B ← A 完成后再次发送,才新开 Turn
一句话:整段对话可以有多个 Turn;一次正常的“你发送 → 我工作 → 本次完成”通常是一个 Turn;流式输出只是这个 Turn 内的多条增量通知。
源码依据:turn/started、turn/completed 与 item/agentMessage/delta · 每条 delta 同时携带 threadId、turnId 与 itemId
短任务若在首次执行中完成,整个目标碰巧只有一个 Turn;但这不是 Goal 的定义。源码明确要求 Goal persists across turns。只要目标仍为 Active,一个 Turn 结束、Thread 恢复空闲后,Goal Runtime 可以调用 start_turn_if_idle(...),以 turn_trigger: "goal" 自动启动下一个续作 Turn。
Goal G:完成一项代码功能
│
└─ 主控 Thread P
├─ Turn P1:理解需求、检查代码、开始实现 → completed
├─ Turn P2:Goal 自动续作、运行测试、修复问题 → completed
└─ Turn P3:验收全部要求、把 Goal 标记 complete → completed
结论:一个 Goal 可以跨多个 Turn;每个 Turn 仍有独立 turnId。
每个子 Agent 都有自己的 Thread,因此也维护自己的 Turn 序列。主控调用 spawn_agent 只是主控当前 Turn 里的一个工具 Item;它同时创建子 Thread,并在子 Thread 中启动一个独立 Turn。子控完成后,结果再送回主控,但两边的 Turn 不会合并成同一个 Turn。
主控 Thread P:Turn P1
spawn_agent(A) ───── wait ───── 收到 A 的结果 ───── 汇总
│
▼
子控 Thread A:Turn A1
编写代码 ───── 调用工具 ───── completed
这是 P1 和 A1 两个 Turn,并行协作;不是一个跨 Thread 的 Turn。
| 主控对子控的操作 | 子控侧是否新建 Turn |
|---|---|
spawn_agent 创建子 Thread 并交付首个任务 | 是:子 Thread 启动自己的首个 Turn。 |
子控仍在运行时使用 send_message | 否:消息交给正在运行的子控,不触发新 Turn。 |
子控已经空闲时使用 followup_task | 是:在同一子 Thread 中触发下一 Turn。 |
主控调用 wait_agent 等待结果 | 不影响计数:它只是主控当前 Turn 里的工具调用。 |
计数原则:先锁定一个 threadId,再数它下面的 turnId。跨 Thread 的一次“派活—回报”不是一个共享 Turn。
源码依据:Goal 明确跨 Turn 持续 · Thread 空闲后启动 Goal 续作 Turn · followup_task 与 send_message 的 Turn 语义 · 对子 Thread 执行 start-or-steer
站级职责:第三站覆盖一整段协作过程——接收请求、排入 Session 队列、由状态拥有者作出判断、返回受理结果。
start_or_steer_turn 只选择 StartOrSteer 路由模式并把请求继续交下去。submission_loop 收件。turn_input::handle 根据模式调用私有函数 start_or_steer;后者先调用 steer_input 检查最新状态,再根据检查结果选择三条主路。Started,追加为 Steered,不能接收为 NotSubmitted。整段链路不做:不在这里请求模型、不在这里运行工具,也不等待答案生成。注意,这是“第三站整段链路”的边界描述,不能改写成“那 7 行公开函数亲自完成了全部判断”。
| 讨论范围 | 准确职责 | 它与状态决策的关系 |
|---|---|---|
| 课程第三站 一整段 Core 路由链 | 从公开入口接收请求,一路送去检查状态、选择分支并拿回受理结果。 | 这是多个函数协作完成的站级职责,不能用“亲自”描述。 |
CodexThread::start_or_steer_turn7 行公开函数 | 把请求和 TurnInputMode::StartOrSteer 交给下层,然后 .await 返回结果。 | 只表达路由意图。函数体没有读取活动 Turn,也没有三分支 match。 |
session::turn_input::start_or_steer主分支协调函数 | 准备设置,先调用 steer_input,再把其结果映射为 Steered、Started 或 NotSubmitted。 | 选择主分支。它有三分支 match,但不直接锁定和读取 active_turn。 |
Session::steer_input状态检查函数 | 锁定 active_turn,检查活动任务是否存在、是否可追加、输入是否为空、Schema 是否兼容。 | 读取真实状态。它返回活动 Turn ID 或具体拒绝原因,供 start_or_steer 选择主分支。 |
| 第三站解决的问题 | 为什么不能让 App Server 直接猜 |
|---|---|
| 用户点击发送时,也许 Thread 空闲,应新开 Turn。 | 状态可能随时变化。若 App Server 先查询“空闲”,过一会儿再提交,查询结果可能已经过期。让拥有状态的 Session 在接件时统一判断,才能依据更近、更一致的状态做决定。 |
| 用户在任务运行中补一句话,也许应追加给当前 Turn。 | |
| 当前是 Review / Compact,或追加内容不兼容,也许必须拒绝。 |
| 名字 | 直白说是什么 | 在这一站干什么 |
|---|---|---|
TurnInputRequest | 包裹里的完整办事材料 | 装“要处理的输入”和随行的设置、上下文、元数据、追踪信息。 |
Submission | 快递外包装 / 投递单 | 给内部操作加上提交 ID,并说明这次要做的操作是 Op::TurnInput。 |
tx_sub → rx_sub | 内存里的投递口和取件口 | 调用者从 tx_sub 投递;Session 的循环从 rx_sub 逐件取出处理。课程把这一整套比喻成“提交邮箱”。 |
reply_tx → reply_rx | 这一个包裹专用的回执通道 | Session 用它只回复一次:已新开、已追加、未受理,或系统错误。它不传模型最终答案。 |
源码里没有一个叫 SessionMailbox 的邮箱对象。这里实际是 async_channel 创建的进程内有界队列,加上持续收件的 submission_loop。它不经过邮箱服务器,不写入你的电子邮箱,也不是 App Server 的 JSON-RPC 网络请求。
turn_start_innerstart_or_steer_turntx_sub → rx_subsubmission_loopstart_or_steersteer_input注意:start_or_steer_turn 是对上游开放的入口;私有函数 start_or_steer 写着“追加、启动、拒绝”的主分支;steer_input 负责锁住并检查真实活动状态。三者都在 Core,但职责粒度不同。
Core 创建新的 Turn 上下文,生成新的 Turn ID,并启动一个 RegularTask。
新工作开始,但尚未执行完成。
新输入进入当前 Turn 的待处理输入队列,不创建新 Turn,返回当前活动 Turn 的 ID。
例如任务运行中,你补一句“先不要改测试”。
例如活动 Turn 正在 Review/Compact、追加的整个 UserInput 列表为空,或要求的输出 Schema 与活动 Turn 不一致。
这是明确拒绝,不会偷偷排队。
此前若把规则概括成“text 不能为空”,这个说法过于绝对。准确规则必须同时说明:哪个客户端、哪种数据、Thread 当前是否有活动 Turn。
turn/start
桌面端不是终端 TUI 的 ChatWidget
params.input 是什么?
input: []turn_start_inner
Vec 可以有 0 项;这里只限制文字总量上限,因此空列表继续交给 Core
start_or_steer 先调用 steer_input
NoActiveTurn它是改走启动分支的控制信号Started创建新 Turn,启动 RegularTaskrun_turn 读取已有 Thread 历史没有新文字 ≠ 没有上下文,所以仍可运行items.is_empty()这时空列表没有可追加的内容NotSubmitted(EmptyInput)不创建新 Turn,也不追加到活动 Turn另一条入口:终端 TUI 若发现文字、本地图片、远程图片同时为空,会在到达 App Server 之前直接返回。
| 你说的“空” | 实际检查对象 | 当前源码中的结果 |
|---|---|---|
| 桌面编辑框没有可见文字 | 只是界面现象 | 不能仅凭界面断定请求中的 input 是不是空列表;桌面端也不是第 1 站那份 TUI ChatWidget。 |
TUI 的 user_message.text 为空 | 文字字段 | 仍可带本地图片或远程图片;只有文字和两类图片同时为空,TUI 才在第 128 行提前返回。 |
App Server 的 params.input 为 [] | Vec<UserInput> 的长度 | input 字段必须存在,但 Vec 可以有 0 项;turn_start_inner 只检查文字总量上限,没有检查下限。 |
Core 收到空 items | 结构化输入列表 + 活动状态 | 没有活动 Turn 时仍可 Started;已有活动 Turn 时不能用空列表 Steering,返回 NotSubmitted(EmptyInput)。 |
Started 分支只在 items 非空时把新的 UserInput 放入 task_input,但无论是否为空,都会启动 RegularTask。后面的 run_turn 会从 sess.clone_history() 取得已有 Thread 历史来构造模型请求,所以“没有新的可见文字”不等于“模型拿到空白上下文”。它更像按了一次“基于现有上下文继续运行”。
不过,没有抓取那一次桌面端的 JSON-RPC 请求前,我们不能断言它送的是 input: [],还是某个界面不可见的结构化输入。源码能确定的是:这两种情况都可能让空闲 Thread 启动。
源码依据:TUI 同时检查文字与图片 · TurnStartParams.input 是 Vec · App Server 只校验文字上限 · 空闲时 Started · 活动 Turn 的空 Steering 被拒绝 · 模型请求读取 Thread 历史
当正在运行的 Turn 使用 sol,你只在界面里把模型选择改成 terra 时,当前 TUI 的正常路径不会重新构造 TurnStartParams,也不会伪造一条空文字。它发送的是另一种 JSON-RPC 请求:thread/settings/update。
AppEvent::UpdateModel("terra") 表示“用户改了模型设置”。这里的 AppEvent 仍然只是 TUI 内部向应用层交接的消息。thread/settings/update { threadId, model: "terra" }。这个参数里没有 input,更不是 TurnStartParams。Op::ThreadSettings,Core 更新 Thread/Session 保存的模型设置。sol;下一次新建 Turn 时才使用 terra。界面立即显示 terra,不等于当前正在执行的 sol 请求被换掉。它表示这个 Thread 的“下一轮默认模型”已经改成 terra。
turn/start(兼具 start-or-steer 语义)携带非空 input → Core 的 start_or_steer → Steered。
这是一次输入提交。
thread/settings/update 携带模型设置 → Op::ThreadSettings → 更新后续 Turn 的设置。
这不是 Steering,也不新建 Turn。
turn/settings/update这个接口专门修改某个正在运行的 Turn 的设置,但它依然不是 turn/start,也不进入 Steering。即使启用,它也只能影响该 Turn 后续尚未捕获设置的模型步骤;已经发出去、正在采样的模型请求不会在半途中从 sol 变成 terra,而且这一轮不保证一定还会发生下一次模型调用。
你当前安装的 Codex 桌面包中可以检索到 thread/settings/update,没有检索到 turn/settings/update。这支持“当前模型选择器更新下一轮模型”的判断;若要证明某一次具体点击发出的原始消息,则需要抓取那次 JSON-RPC 请求。
源码依据:TUI 的 AppEvent::UpdateModel · thread/settings/update 请求构造 · 设置用于 subsequent turns · App Server 转成 Op::ThreadSettings · 实验性的 turn/settings/update
pub async fn start_or_steer_turn(
&self,
request: TurnInputRequest,
) -> CodexResult<TurnInputSubmission> {
self.submit_turn_input_with_mode(request, TurnInputMode::StartOrSteer)
.await
}
| 代码片段 | 逐词含义 | 架构意义 |
|---|---|---|
pub | 公开可见 | 这是 CodexThread 提供给 App Server 等调用方的 Core API。 |
async fn | 异步函数 | 调用方用 .await 等待 Core 的路由决定。 |
&self | 借用当前 CodexThread | 不会取得 Thread 所有权,后续仍可共享使用。 |
request: TurnInputRequest | 参数名与类型 | App Server 已经把协议输入整理成 Core 的 Turn 输入信封。 |
CodexResult<...> | Result<..., CodexErr> 的别名 | 外层区分 Core 内部错误与正常路由结果。 |
TurnInputMode::StartOrSteer | 枚举类型的一个变体 | 它把“路由策略”作为数据传下去:空闲则启动,忙碌且可追加则 Steering。 |
.await 后没有分号 | 这是函数体最后一个表达式 | Rust 直接把下游返回值作为本函数返回值;加分号后会变成 (),类型就不对了。 |
它隐藏了 Thread 内部状态,让上游只表达意图:“请你自己判断启动还是追加”。这叫门面式 API。状态判断集中在 Core 内部,App Server 不需要先查询状态再决定,避免查询后状态又变化的竞态窗口。
源码依据:CodexThread::start_or_steer_turn
TurnInputRequest:不是一句话,而是一整包内部请求TurnStartParams 分开TurnStartParams 是 App Server 对外协议里的“客户申请表”;TurnInputRequest 是 App Server 校验、转换以后,交给 Core 的“内部办事包”。两者表达的是同一次发送,但所处层级、字段类型和使用者不同。
pub struct TurnInputRequest {
pub input: TurnInput,
pub thread_settings: ThreadSettingsOverrides,
pub start: TurnStartOptions,
pub additional_context: BTreeMap<String, AdditionalContextEntry>,
pub responsesapi_client_metadata: Option<HashMap<String, String>>,
pub trace: Option<W3cTraceContext>,
}
真正难懂的不是“它有六个字段”,而是这些字段有层次。普通文字“帮我检查登录模块”实际被包了三层:
TurnInput 里面又有 UserInputTurnInputRequest.input 不是一段字符串,它的类型是 TurnInput。TurnInput 可以表示普通用户输入、已有的 Responses Item 或智能体间通信;但本课的 start_or_steer 只接受其中的 UserInput 形态。这个形态里又有 content: Vec<UserInput>,所以一条消息可以同时装文字、图片、音频、Skill 或 Mention 等多个输入项。
| 字段 | 它回答什么问题 | 普通情况下怎么理解 |
|---|---|---|
input | 这次交来什么? | 这是主件。普通发送是 TurnInput::UserInput,里面的 content 列表才装文字、图片等实际内容。Started 时它成为新 Turn 的输入;Steered 时进入活动 Turn 的待处理输入。 |
thread_settings | 这个 Thread 今后按什么设置工作? | 模型、推理强度、审批、权限、沙箱、环境、协作模式等“覆盖值”。没传的字段通常保留原值。请求若被接受,这些覆盖会写入 Thread 设置;Steered 时不会倒改已经运行的 Turn,主要供后续 Turn 使用。 |
start | 如果新开 Turn,这一轮有什么专属要求? | 包含触发来源、结构化输出 Schema、单轮服务档位、父/根 Turn 关系等。Started 才正式记录;若正在 Steering,输出 Schema 和 lineage 等会用于检查是否与活动 Turn 兼容。 |
additional_context | 除了用户消息,还要补哪些背景? | 按来源 ID 保存的补充上下文,每项有文本值和来源类别。无论 Started 还是 Steered,成功时都会合并进输入。 |
responsesapi_client_metadata | 调用模型服务时带哪些客户端标签? | 可选的字符串键值表,随 Responses API 请求传递。它是元数据,不是用户正文,也不是让模型执行的自然语言指令。 |
trace | 日志里怎样认出这是同一次请求? | 可选的 W3C Trace Context。它像物流追踪号,让跨 App Server、Session 队列和后续处理的日志能够关联;它不决定模型回答内容。 |
高效记法:input 是主件;thread_settings 是 Thread 今后的运行规则;start 是“真开新 Turn 才落地”的本轮选项;其余三个分别是补充材料、服务标签和追踪号。
new,再连续 with...?TurnInputRequest::new(TurnInput::UserInput { ... })
.with_thread_settings(thread_settings)
.on_start(TurnStartOptions { ... })
.with_additional_context(additional_context)
.with_responses_metadata(metadata)
.with_trace(trace)
new(...) 强制先给出不可缺少的主件 input,并把其余字段设为默认值:设置和启动选项为空覆盖,补充上下文为空表,元数据与追踪信息为 None。后面的 Builder 方法每次补一类可选材料,再把整个对象返回,所以能连着写。
这也说明:六个字段都存在于结构体中,不等于客户端每次都显式填写六份内容。很多字段只是保持默认值。
源码依据:TurnInputRequest 与 Builder
每个运行中的 Session 会创建一对内存通道:tx_sub 是发送端,rx_sub 是接收端。CodexThread 内部的 I/O 句柄拿着发送端投递 Submission;后台的 submission_loop 守着接收端,收到一件就查看其中的 op,再交给对应处理函数。
因此,“提交邮箱”不是某一个变量,而是发送端 + 有界队列 + 接收端 + 持续收件循环这套机制的通俗合称。
let id = new_submission_id();
let (reply_tx, reply_rx) = oneshot::channel();
self.submit_with_id(Submission {
id,
op: Op::TurnInput {
request: Box::new(request),
mode,
reply: reply_tx,
},
...
}).await?;
reply_rx.await.unwrap_or(Err(CodexErr::InternalAgentDied))
上面是寄件方。下面是 Session 收件方的关键代码:
while let Ok(sub) = rx_sub.recv().await {
match sub.op {
Op::TurnInput { request, mode, reply } => {
let result = turn_input::handle(...).await;
let _ = reply.send(result);
}
// Interrupt、审批回复、Shutdown 等其他内部操作也从这里分发
}
}
new_submission_id() 给这次投递生成唯一 ID;如果最终新开 Turn,这个 ID 也成为新 Turn ID。oneshot::channel() 创建一条只能回一次的专用回执线;reply_tx 随包裹寄走,reply_rx 留给调用者。Submission 把 ID、操作 Op::TurnInput、请求、路由模式和回执发送端装在一起。submit_with_id 最终执行 tx_sub.send(sub).await,把它放进 Session 的提交队列。submission_loop 从 rx_sub 取出,调用 turn_input::handle 做状态路由,再用 reply.send(result) 寄回受理结果。reply_rx.await 等到回执后返回;这时模型任务可能才刚启动。同一 Session 还可能收到追加输入、中断、审批回复、设置更新和关闭等操作。它们若从不同调用方同时直接修改状态,就更容易发生先后冲突。统一投递后,submission_loop 按收件顺序分发,状态判断集中在拥有 Session 状态的位置。
不等于。串行的是“收件和路由决定”。当 Started 分支调用 spawn_task 后,模型与工具工作在后续异步任务中继续。本邮箱没有等完整 Turn 结束才收下一封。
| 容易误解的点 | 准确含义 |
|---|---|
Submission 就是 TurnInputRequest 吗? | 不是。TurnInputRequest 是办事材料;Submission 是更外层的内部投递信封。它的 op 里才装着 boxed request、路由模式和回复端。 |
Box::new(request) 是发到网络吗? | 不是。它把较大的请求放到堆上,并把所有权交给 Op::TurnInput;仍在当前 Codex 进程内。 |
第一个 .await? 在等什么? | 等请求成功放入 Session 队列;若发送端已关闭,转成 InternalAgentDied 向上返回。 |
reply_rx.await 在等什么? | 等 Session 对这一次提交给出 Started / Steered / NotSubmitted 路由回执,不等模型回答。 |
| 为什么叫“有界队列”? | 源码创建队列时容量是 512。队列满时,发送方的 send(...).await 会等待空位,避免无限制地把提交堆在内存里。 |
最高效的记法:请求包是“做什么”;Submission 是“投递哪种内部操作”;提交队列是“按次序送到状态拥有者”;oneshot 是“只回一次受理结果”。
源码依据:Session 创建容量 512 的提交通道 · 发送 Op::TurnInput 并等待一次性回复 · Session submission loop
match session.steer_input(...).await {
Ok(turn_id) => {
settings.apply_steered(...).await?;
Ok(TurnInputSubmission::Steered { turn_id })
}
Err(NotSubmittedReason::NoActiveTurn) => {
let turn_context = settings.apply_started(...).await?;
session.spawn_task(turn_context, task_input, RegularTask::new()).await;
Ok(TurnInputSubmission::Started { turn_id: submission_id })
}
Err(reason) => Ok(TurnInputSubmission::NotSubmitted { reason }),
}
Session::steer_input 加锁读取 active_turn,检查活动任务类型、空输入和 Schema 等条件,返回 Ok(turn_id) 或具体的 NotSubmittedReason。上面的 start_or_steer 不直接读取这些字段,而是根据返回值选择动作:追加成功、没有活动 Turn 就启动、其他原因就拒绝。
NoActiveTurn 在这里只是内部控制信号,不是最终拒绝结果。只有其他无法追加的原因,才会作为 NotSubmitted 返回上游。
| 当前状态 | Core 结果 | Turn ID | 是否创建 RegularTask |
|---|---|---|---|
| 没有活动 Turn,输入非空 | Started | 新的 submission_id | 是 |
没有活动 Turn,items 为空 | Started | 新的 submission_id | 是;没有新增用户输入项,仍可基于已有上下文运行 |
| 普通 Turn 活动中,输入非空且兼容 | Steered | 原活动 Turn ID | 否,输入加入当前 Turn |
| Review 或 Compact Turn 活动中 | NotSubmitted(ActiveTurnNotSteerable) | 无新 ID | 否 |
| 活动 Turn + 空输入 | NotSubmitted(EmptyInput) | 无新 ID | 否 |
| 活动 Turn 的输出 Schema 不兼容 | NotSubmitted(ActiveTurnOutputSchemaMismatch) | 无新 ID | 否 |
源码依据:路由入口与 start_or_steer · steer_input 的活动 Turn 检查
CodexResult<TurnInputSubmission>
// 系统/程序级错误
Err(CodexErr::InternalAgentDied)
// 系统正常运行,但业务路由明确拒绝
Ok(TurnInputSubmission::NotSubmitted {
reason: NotSubmittedReason::EmptyInput,
})
这对产品设计非常重要:系统故障和状态不允许是两类问题。前者需要重试、报警或恢复;后者应该给用户可理解的原因和下一步操作,而不是统一显示“发生未知错误”。
Started 和 Steered 也只表示 Core 接受了输入。源码明确说明:它们不等待 Prompt Hook、模型上下文更新、持久化或模型采样,更不代表 Turn 完成。
源码依据:TurnInputSubmission 与拒绝原因
LLMCommandBar.tsx 已有 pendingSmartInput,并通过 shouldContinuePendingSmartCommand 判断用户的新文字是在补充待完成指令,还是在发起另一条命令。
| Codex | GEODYNA 当前雏形 | 未来智能体应补齐 |
|---|---|---|
| 活动 Turn | pendingSmartInput 表示尚缺参数的智能指令 | 显式的 Agent Task 状态与任务 ID |
Steered | 把补充文字与之前的用户指令合并后重新分类 | 把兼容的新约束安全注入正在运行的任务,并保留审计记录 |
Started | 把输入作为一条新 Smart Command 分类和执行 | 新建任务上下文,经过预览、确认和正式 Command API 执行 |
NotSubmitted(reason) | needsInput、unsupported、确认与撤销失败提示 | 结构化拒绝原因:不可追加、冲突、危险操作待确认、项目状态过期 |
产品判断:工程软件不能把所有“运行中再次输入”都默认为 Steering。若当前任务正处于不可逆提交边界,应明确拒绝或要求用户选择“追加约束、排队为下一任务、先中断当前任务”。
GEODYNA 只读依据:D:\GEODAMNEW\components\LLMCommandBar.tsx:318 的延续判断,以及 :692 开始的发送路由。
start_or_steer 先尝试哪条路径?Ok(NotSubmitted) 是否等于 Core 系统故障?reply_rx.await 等到的是什么?input: [],Core 一定返回 EmptyInput 吗?TurnInputRequest 的哪条嵌套路径里?按你的理解补完整,不要照抄。先说“东西是什么”,再说“流程做什么”:
对象:TurnInputRequest 是 ______;普通文字位于 input → ______ → content → ______。所谓“提交邮箱”实际是 ______,oneshot 通道只负责返回 ______。
职责:start_or_steer_turn 位于 ______ 层。它让 Session 根据最新状态决定 ______、______ 或 ______;这些结果都不表示 ______。