From f121257cca0594e0144b5892501f9f0516db7d79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 30 Sep 2026 15:47:22 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BA=BF=E7=A8=8B=E7=AE=A1=E7=90=86=E5=99=A8?= =?UTF-8?q?=E5=BD=92=E4=BD=8D=20agent/thread=5Fmanager=EF=BC=9A=E5=8E=BB?= =?UTF-8?q?=E6=8E=89=20Direct=20=E5=89=8D=E7=BC=80=EF=BC=8C=E6=8C=89?= =?UTF-8?q?=E8=81=8C=E8=B4=A3=E6=8B=86=E6=96=87=E4=BB=B6=20-=20=E5=9B=9B?= =?UTF-8?q?=E4=B8=AA=20direct=5F*=20=E6=96=87=E4=BB=B6=E7=A7=BB=E5=85=A5?= =?UTF-8?q?=20agent/thread=5Fmanager=EF=BC=9Awire.rs=EF=BC=88=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=80=81=E4=BA=8B=E4=BB=B6=E5=A5=91=E7=BA=A6=EF=BC=89?= =?UTF-8?q?=E3=80=81queue.rs=EF=BC=88=E5=BE=85=E5=8F=91=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E9=98=9F=E5=88=97=E8=A7=84=E5=88=99=EF=BC=89=E3=80=81dispatch.?= =?UTF-8?q?rs=EF=BC=88=E6=94=BE=E8=A1=8C=E4=B8=8E=E5=8D=A0=E7=94=A8?= =?UTF-8?q?=EF=BC=89=E3=80=81mod.rs=EF=BC=88=E7=AE=A1=E7=90=86=E5=99=A8?= =?UTF-8?q?=E4=B8=BB=E4=BD=93=E4=B8=8E=E5=AF=B9=E5=A4=96=E5=85=A5=E5=8F=A3?= =?UTF-8?q?=EF=BC=89=20-=20=E7=B1=BB=E5=9E=8B=E5=8E=BB=E6=8E=89=20Direct?= =?UTF-8?q?=20=E5=89=8D=E7=BC=80=EF=BC=9AThreadEvent=20/=20ThreadManager?= =?UTF-8?q?=20/=20ThreadItem=20/=20ThreadFileChange=20/=20ThreadDeltaKind?= =?UTF-8?q?=20/=20ThreadRequestKind=20/=20PendingTurn=20/=20QueueRemovalRe?= =?UTF-8?q?ason=20/=20QueueRemovalOutcome=20/=20DispatchedTurn=20/=20TurnR?= =?UTF-8?q?eservation=20/=20TurnIdentity=20/=20StaleTurnRelease=20/=20Cons?= =?UTF-8?q?umeResult=20/=20SubscriptionBootstrap=20/=20ActiveTurn=20/=20Ac?= =?UTF-8?q?tiveTurnSnapshot=20-=20=E5=AF=B9=E5=A4=96=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E6=94=B9=E5=90=8D=EF=BC=9Asubscribe=5Fthread=20/=20consume=5Ft?= =?UTF-8?q?hread=20/=20unsubscribe=5Fthread=20/=20append=5Fthread=5Fevent?= =?UTF-8?q?=20/=20enqueue=5Fpending=5Fturn=20/=20remove=5Fpending=5Fturn?= =?UTF-8?q?=20/=20claim=5Fpending=5Fturn=20/=20update=5Factive=5Fturn=20/?= =?UTF-8?q?=20list=5Factive=5Fturns=20/=20complete=5Fturn=20/=20complete?= =?UTF-8?q?=5Fturn=5Fif=5Freserved=20/=20active=5Fturn=5Fid=5Fat=20/=20rea?= =?UTF-8?q?d=5Fturn=5Fidentity=20/=20stale=5Fturn=5Ffor=5Frelease=20/=20th?= =?UTF-8?q?read=5Fid=5Ffor=5Fproject=20-=20=E7=94=9F=E6=88=90=E7=BB=91?= =?UTF-8?q?=E5=AE=9A=E8=B7=9F=E7=9D=80=E6=94=B9=E5=90=8D=EF=BC=88DirectThr?= =?UTF-8?q?eadEvent=E2=86=92ThreadEvent=20=E7=AD=89=E5=8D=81=E4=B8=AA?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=BA=20rename=EF=BC=89=EF=BC=8C=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E5=85=AB=E5=A4=84=20import=20=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=EF=BC=9BTauri=20=E5=91=BD=E4=BB=A4=E5=90=8D=E4=B8=8E=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=20IPC=20=E5=A5=91=E7=BA=A6=E4=B8=8D=E5=8F=98=20-=20?= =?UTF-8?q?=E5=AE=9A=E5=90=91=E9=AA=8C=E8=AF=81=EF=BC=9Acargo=20test=20age?= =?UTF-8?q?nt::=EF=BC=88945=20passed=EF=BC=8C=E5=8F=A6=E4=B8=A4=E6=9D=A1?= =?UTF-8?q?=E5=B9=B6=E5=8F=91=E7=94=A8=E4=BE=8B=E4=B8=BA=E6=97=A2=E6=9C=89?= =?UTF-8?q?=20flaky=E3=80=81=E9=87=8D=E8=B7=91=E9=80=9A=E8=BF=87=EF=BC=89?= =?UTF-8?q?=E3=80=81npx=20vitest=20run=20appSurface/directThreadChat/direc?= =?UTF-8?q?tHistoryPaging=EF=BC=88247=20passed=EF=BC=89=E3=80=81typecheck?= =?UTF-8?q?=20=E9=80=9A=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src-tauri/src/agent.rs | 16 +- .../direct_project_identity.rs | 2 +- .../src/agent/codex_app_server/mod.rs | 144 +++-- .../src-tauri/src/agent/direct_execution.rs | 2 +- .../src/agent/direct_project_context.rs | 19 +- .../src/agent/direct_project_history.rs | 6 +- .../src-tauri/src/agent/direct_runtime/mod.rs | 109 ++-- .../src/agent/direct_runtime/user_input.rs | 64 ++- .../src-tauri/src/agent/direct_tool_bridge.rs | 8 +- .../src-tauri/src/agent/direct_tool_calls.rs | 2 +- .../src-tauri/src/agent/direct_tools_mcp.rs | 8 +- .../src-tauri/src/agent/direct_turn_error.rs | 4 +- .../src/agent/direct_turn_failure.rs | 18 +- .../src-tauri/src/agent/direct_validation.rs | 4 +- .../src/agent/generation/prompt_context.rs | 2 +- .../src/agent/runtime_driver/entrypoints.rs | 4 +- .../dispatch.rs} | 127 +++-- .../mod.rs} | 509 ++++++++---------- .../queue.rs} | 28 +- .../wire.rs} | 264 +++++---- .../src-tauri/src/commands.rs | 36 +- .../src-tauri/src/main.rs | 2 +- .../useDirectProjectChatController.ts | 46 +- .../useDirectThreadChatSubscription.ts | 14 +- .../chat/conversation/directThreadChat.ts | 32 +- .../directThreadItemProjection.ts | 10 +- .../chat/generated/ConsumeResult.ts | 4 + .../generated/DirectThreadConsumeResult.ts | 4 - ...tThreadHistorySlice.ts => HistorySlice.ts} | 6 +- ...movalOutcome.ts => QueueRemovalOutcome.ts} | 5 +- ...RemovalReason.ts => QueueRemovalReason.ts} | 2 +- ...nBootstrap.ts => SubscriptionBootstrap.ts} | 6 +- ...tThreadDeltaKind.ts => ThreadDeltaKind.ts} | 2 +- .../{DirectThreadEvent.ts => ThreadEvent.ts} | 27 +- ...hreadFileChange.ts => ThreadFileChange.ts} | 2 +- .../{DirectThreadItem.ts => ThreadItem.ts} | 8 +- ...eadRequestKind.ts => ThreadRequestKind.ts} | 2 +- .../chat/history/directHistoryPaging.ts | 14 +- .../tests/directHistoryPaging.test.ts | 20 +- .../tests/directThreadChat.test.ts | 32 +- 40 files changed, 747 insertions(+), 867 deletions(-) rename apps/ai-game-creator-shell/src-tauri/src/agent/{direct_turn_dispatch.rs => thread_manager/dispatch.rs} (76%) rename apps/ai-game-creator-shell/src-tauri/src/agent/{direct_thread_manager.rs => thread_manager/mod.rs} (80%) rename apps/ai-game-creator-shell/src-tauri/src/agent/{direct_thread_queue.rs => thread_manager/queue.rs} (91%) rename apps/ai-game-creator-shell/src-tauri/src/agent/{direct_thread_wire.rs => thread_manager/wire.rs} (87%) create mode 100644 apps/ai-game-creator-shell/src/view/project-development/chat/generated/ConsumeResult.ts delete mode 100644 apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadConsumeResult.ts rename apps/ai-game-creator-shell/src/view/project-development/chat/generated/{DirectThreadHistorySlice.ts => HistorySlice.ts} (73%) rename apps/ai-game-creator-shell/src/view/project-development/chat/generated/{DirectQueueRemovalOutcome.ts => QueueRemovalOutcome.ts} (62%) rename apps/ai-game-creator-shell/src/view/project-development/chat/generated/{DirectQueueRemovalReason.ts => QueueRemovalReason.ts} (85%) rename apps/ai-game-creator-shell/src/view/project-development/chat/generated/{DirectThreadSubscriptionBootstrap.ts => SubscriptionBootstrap.ts} (71%) rename apps/ai-game-creator-shell/src/view/project-development/chat/generated/{DirectThreadDeltaKind.ts => ThreadDeltaKind.ts} (71%) rename apps/ai-game-creator-shell/src/view/project-development/chat/generated/{DirectThreadEvent.ts => ThreadEvent.ts} (85%) rename apps/ai-game-creator-shell/src/view/project-development/chat/generated/{DirectThreadFileChange.ts => ThreadFileChange.ts} (85%) rename apps/ai-game-creator-shell/src/view/project-development/chat/generated/{DirectThreadItem.ts => ThreadItem.ts} (89%) rename apps/ai-game-creator-shell/src/view/project-development/chat/generated/{DirectThreadRequestKind.ts => ThreadRequestKind.ts} (87%) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent.rs b/apps/ai-game-creator-shell/src-tauri/src/agent.rs index cb2fc5ae7..2e8e3ee12 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -29,13 +29,9 @@ mod direct_project_context; mod direct_project_history; mod direct_project_turn_history; mod direct_runtime; -mod direct_thread_manager; -mod direct_thread_queue; -mod direct_thread_wire; mod direct_tool_bridge; mod direct_tool_calls; mod direct_tools_mcp; -mod direct_turn_dispatch; mod direct_turn_error; mod direct_turn_failure; mod direct_turn_stream; @@ -50,6 +46,8 @@ mod runtime_protocol; mod runtime_state; mod runtime_tools; mod skill_pack; +mod thread_manager; + use claude_code_cli::*; pub(crate) use claude_code_cli::{ cancel_direct_claude_code_turn_at, direct_game_creator_claude_code_chat_at, @@ -59,7 +57,7 @@ pub(crate) use claude_code_cli::{ use codex_app_server::*; pub(crate) use codex_app_server::{ cancel_direct_codex_turn_at, direct_game_creator_codex_chat_at, - direct_game_creator_home_codex_chat, direct_thread_id_for_project, DirectTurnCancelView, + direct_game_creator_home_codex_chat, thread_id_for_project, DirectTurnCancelView, }; use codex_cli::*; pub(crate) use codex_cli::{ @@ -72,13 +70,9 @@ pub(crate) use direct_codex_user_item::*; pub(crate) use direct_project_history::*; pub(crate) use direct_project_turn_history::*; pub(crate) use direct_runtime::*; -pub(crate) use direct_thread_manager::*; -pub(crate) use direct_thread_queue::*; -pub(crate) use direct_thread_wire::*; pub(crate) use direct_tool_bridge::*; pub(crate) use direct_tool_calls::*; pub(crate) use direct_tools_mcp::*; -pub(crate) use direct_turn_dispatch::*; pub(crate) use direct_turn_error::*; pub(crate) use direct_turn_failure::*; pub(crate) use direct_turn_stream::*; @@ -93,6 +87,10 @@ pub(crate) use runtime_protocol::*; pub(crate) use runtime_state::*; pub(crate) use runtime_tools::*; pub(crate) use skill_pack::*; +pub(crate) use thread_manager::dispatch::*; +pub(crate) use thread_manager::queue::*; +pub(crate) use thread_manager::wire::*; +pub(crate) use thread_manager::*; pub(crate) fn shutdown_game_creator_codex_app_servers() -> Result<(), String> { shutdown_game_creator_codex_app_servers_impl() diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/direct_project_identity.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/direct_project_identity.rs index 98f347d30..cf90638c4 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/direct_project_identity.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/direct_project_identity.rs @@ -30,7 +30,7 @@ pub(crate) fn direct_codex_canonical_project_identity( /// `direct_codex_canonical_project_identity`)的要求,而线程 id 只是一个路径 key。把 /// manifest 的瞬时抖动混进线程 id,会让同一项目在"订阅那一刻"与"跑回合那一刻"算出两个 /// 字符串(例如调用方给的是符号链接路径),订阅就绑到一条永远不会有事件的空线程上。 -pub(crate) fn direct_thread_id_for_project(root: &std::path::Path) -> String { +pub(crate) fn thread_id_for_project(root: &std::path::Path) -> String { let Ok((canonical_root, _)) = resolve_direct_codex_project_authority(root) else { return root.to_string_lossy().into_owned(); }; diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs index eb82b1cc1..3c00d1410 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs @@ -16,7 +16,7 @@ use process_tree::{OwnedProcessTree, ProcessTreeExitProof}; mod direct_project_identity; mod execution; mod model_catalog; -pub(crate) use direct_project_identity::direct_thread_id_for_project; +pub(crate) use direct_project_identity::thread_id_for_project; use direct_project_identity::*; use execution::ExecutionAdapter; @@ -631,7 +631,7 @@ enum CodexTurnEvent { params: serde_json::Value, }, Request { - kind: DirectThreadRequestKind, + kind: ThreadRequestKind, params: serde_json::Value, }, RawItem(serde_json::Value), @@ -808,8 +808,8 @@ fn direct_codex_safe_activity_for_item_value(item: &serde_json::Value) -> &'stat fn direct_thread_event_item( root: &std::path::Path, item: &serde_json::Value, -) -> Option { - direct_thread_item_from_value(root, item, direct_tool_call_now_ms()) +) -> Option { + thread_item_from_value(root, item, direct_tool_call_now_ms()) } /// 运行态条目投影:Codex 回显的用户消息整条跳过。 @@ -824,7 +824,7 @@ fn direct_thread_event_item( fn direct_thread_visible_item( root: &std::path::Path, item: &serde_json::Value, -) -> Option { +) -> Option { if is_direct_project_codex_user_item(item) { return None; } @@ -844,14 +844,14 @@ fn direct_thread_visible_item( pub(crate) fn emit_direct_thread_user_item( root: &std::path::Path, item: &serde_json::Value, -) -> Option { +) -> Option { let entry_item = direct_thread_event_item(root, item)?; // 条目时间是落盘 / 观测时间。前端乐观用户气泡已删(ADR「DirectProject命令入队化」), // 所以这就是界面显示这条用户消息的唯一时间口径:它晚于用户按下发送,但不再有第二份更早的时间。 let at = entry_item.at(); - append_direct_thread_event( - &direct_thread_id_for_project(root), - DirectThreadEvent::item_completed(entry_item.clone(), at), + append_thread_event( + &thread_id_for_project(root), + ThreadEvent::item_completed(entry_item.clone(), at), ); Some(entry_item) } @@ -1080,21 +1080,21 @@ fn direct_codex_safe_activity_for_notification(method: &str) -> Option<&'static } } -fn direct_codex_request_event_type(method: &str) -> Option { +fn direct_codex_request_event_type(method: &str) -> Option { match method { "item/fileChange/requestApproval" | "item/commandExecution/requestApproval" - | "item/permissions/requestApproval" => Some(DirectThreadRequestKind::ApprovalRequested), + | "item/permissions/requestApproval" => Some(ThreadRequestKind::ApprovalRequested), "item/tool/requestUserInput" | "item/mcpToolCall/requestUserInput" => { - Some(DirectThreadRequestKind::AskRequested) + Some(ThreadRequestKind::AskRequested) } _ => None, } } -fn direct_codex_resolution_event_type(method: &str) -> Option { +fn direct_codex_resolution_event_type(method: &str) -> Option { match method { - "serverRequest/resolved" => Some(DirectThreadRequestKind::RequestResolved), + "serverRequest/resolved" => Some(ThreadRequestKind::RequestResolved), _ => None, } } @@ -1197,10 +1197,10 @@ fn direct_codex_reasoning_delta_event( fn direct_codex_thread_delta_event( root: &std::path::Path, item_id: String, - kind: DirectThreadDeltaKind, + kind: ThreadDeltaKind, delta: &str, -) -> DirectThreadEvent { - DirectThreadEvent::item_delta(item_id, kind, direct_thread_delta_text(root, delta)) +) -> ThreadEvent { + ThreadEvent::item_delta(item_id, kind, thread_delta_text(root, delta)) } /// 通知 → 回合事件的唯一分类函数:运行态读取器与单测共用这一份。 /// @@ -3616,14 +3616,14 @@ impl CodexAppServerConnection { } } turn_start_guard.armed = false; - let direct_thread_id = direct_thread_id_for_project(history_root); + let direct_thread_id = thread_id_for_project(history_root); // 本轮开口用户条目的 canonical id:只从已落盘的那条条目上读身份(`id`,工具条目才用 // `call_id`),不在事件侧重造一份。拿不到就留空,让前端按"归属不可证明"处理。 let direct_turn_user_item_id = direct_turn_user_item .as_ref() - .and_then(direct_thread_item_identity); + .and_then(thread_item_identity); // 逻辑回合的**边界**不在这里:开始事件由认领动作发出、兜底由放行占用对象持有 - // (`direct_turn_dispatch.rs`)。本轮的用户条目也不在这里下发——发点在放行之后、 + // (`thread_manager::dispatch`)。本轮的用户条目也不在这里下发——发点在放行之后、 // 起 codex 之前(`emit_direct_thread_user_item`),见那条注释。 // // 下面这个毫秒钟与逻辑回合无关,只服务模型终态的**完成时刻**:上游 Turn 的 @@ -3713,14 +3713,14 @@ impl CodexAppServerConnection { Some(CodexTurnEvent::AgentMessageDelta { item_id, delta }) => { if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject { direct_project_history.observe_delta(&item_id, &delta); - append_direct_thread_event( + append_thread_event( &direct_thread_id, // 事件自足:增量自带 item 身份与正文类别(正文 / 思考), // 前端 reducer 不允许靠猜 itemId 的来源决定 kind。 direct_codex_thread_delta_event( history_root, item_id.clone(), - DirectThreadDeltaKind::Message, + ThreadDeltaKind::Message, &delta, ), ); @@ -3755,12 +3755,12 @@ impl CodexAppServerConnection { } Some(CodexTurnEvent::ReasoningDelta { item_id, delta }) => { if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject { - append_direct_thread_event( + append_thread_event( &direct_thread_id, direct_codex_thread_delta_event( history_root, item_id, - DirectThreadDeltaKind::Reasoning, + ThreadDeltaKind::Reasoning, &delta, ), ); @@ -3797,9 +3797,9 @@ impl CodexAppServerConnection { if let Some(entry_item) = entry_item { // `rawResponseItem/completed` 不带阶段时间,宿主处理到这条 // 通知的钟就是该阶段唯一可证明的时间。 - append_direct_thread_event( + append_thread_event( &direct_thread_id, - DirectThreadEvent::item_completed( + ThreadEvent::item_completed( entry_item, direct_tool_call_now_ms(), ), @@ -3815,9 +3815,9 @@ impl CodexAppServerConnection { .or_else(|| params.get("id").and_then(serde_json::Value::as_str)) .filter(|value| !value.is_empty()) .map(str::to_string); - append_direct_thread_event( + append_thread_event( &direct_thread_id, - DirectThreadEvent::request(kind, request_id), + ThreadEvent::request(kind, request_id), ); } } @@ -3930,11 +3930,11 @@ impl CodexAppServerConnection { { // `item/started` 的通知层带 `startedAtMs`:这是工具真正 // 开始的阶段时间,优先于条目展示时间与宿主钟。 - append_direct_thread_event( + append_thread_event( &direct_thread_id, - DirectThreadEvent::item_started( + ThreadEvent::item_started( entry_item, - direct_thread_item_event_at_ms( + thread_item_event_at_ms( ¶ms, item, false, @@ -3984,7 +3984,7 @@ impl CodexAppServerConnection { { model_terminal = Some(( status.to_string(), - direct_thread_turn_completed_at_ms( + thread_turn_completed_at_ms( turn, Some(direct_turn_started_at_ms), direct_tool_call_now_ms(), @@ -4311,7 +4311,7 @@ impl DirectTurnTerminalContext { history_root, ); // 终态走 Thread Manager 的深出口:解除这一轮的占用并写下 `turn.completed`。 - complete_direct_thread_turn( + complete_turn( &self.thread_id, terminal.event(self.completed_at, self.user_item_id.as_deref()), ); @@ -4502,7 +4502,7 @@ enum DirectCodexTurnCancelTarget { /// app-server 侧还有活句柄:正常发 `turn/interrupt`。 Interrupt(Arc), /// app-server 侧已经拿不到可中断的活句柄;带上是哪种情况。 - Stale(DirectStaleTurnReleaseReason), + Stale(StaleTurnReleaseReason), } /// 终止当前项目正在运行的 Direct 回合。 @@ -4515,7 +4515,7 @@ enum DirectCodexTurnCancelTarget { /// 只发中断会让 Thread Manager 上这一轮的占用永远解不开,这个项目此后每条消息都会被 /// "已有另一条回合正在跑"挡在放行之外——这正是"重进会话被堵死"的死锁形态。这时显式写一条 /// `turn.completed{aborted}` 解除占用并把可读原因返回给界面。释放条件见 -/// [`direct_stale_turn_for_release`] 的注释;"正在跑的是另一轮"仍然保持原拒绝语义, +/// [`stale_turn_for_release`] 的注释;"正在跑的是另一轮"仍然保持原拒绝语义, /// 什么都不释放。 /// /// 兜底终态由 Thread Manager 在解除占用的同一个临界区里写下(带 `userItemId`,身份取被释放那一轮 @@ -4552,12 +4552,10 @@ pub(crate) fn cancel_direct_codex_turn_at( Ok((_, cancellation)) if cancellation.app_server_alive() => { DirectCodexTurnCancelTarget::Interrupt(Arc::clone(cancellation)) } - Ok(_) => { - DirectCodexTurnCancelTarget::Stale(DirectStaleTurnReleaseReason::ExecutorExited) + Ok(_) => DirectCodexTurnCancelTarget::Stale(StaleTurnReleaseReason::ExecutorExited), + Err(_) => { + DirectCodexTurnCancelTarget::Stale(StaleTurnReleaseReason::NeverReachedExecutor) } - Err(_) => DirectCodexTurnCancelTarget::Stale( - DirectStaleTurnReleaseReason::NeverReachedExecutor, - ), } }; match target { @@ -4577,10 +4575,10 @@ pub(crate) fn cancel_direct_codex_turn_at( // 校验与释放**同一个临界区**(确实登记着一轮、身份对得上、过了启动窗口才允许判成残留; // 占用与兜底终态要么一起落地,要么一个字都不写)。校验完了再单独释放不行:两步之间并发 // 认领可能已经登记了新那一轮的占用,无条件解除就会清掉它。 - let released = direct_stale_turn_for_release(root, client_turn_id, reason)?; + let released = stale_turn_for_release(root, client_turn_id, reason)?; // 这一轮不会再有人替它收尾(占用刚被兜底解除):踢一脚让队列继续。 // 少了这一脚,排在这条后面的待发消息要等到下一次用户动作才会被放行。 - crate::agent::kick_direct_queue_dispatch(root); + crate::agent::kick_queue_dispatch(root); Ok(DirectTurnCancelView { outcome: DIRECT_TURN_CANCEL_OUTCOME_RELEASED.to_string(), message: format!( @@ -6004,10 +6002,7 @@ mod tests { fn direct_thread_message_and_reasoning_deltas_are_sanitized_before_enqueue() { let root = std::path::Path::new("/workspace/direct-project"); let raw = "key=sk-abcdefghijklmnop project=/workspace/direct-project/assets/a.png private=/root/secret.txt"; - for kind in [ - DirectThreadDeltaKind::Message, - DirectThreadDeltaKind::Reasoning, - ] { + for kind in [ThreadDeltaKind::Message, ThreadDeltaKind::Reasoning] { let event = direct_codex_thread_delta_event(root, "item-1".to_string(), kind, raw); let wire = serde_json::to_string(&event).expect("serialize direct thread delta"); assert!( @@ -6043,12 +6038,12 @@ mod tests { ]; let streamed: String = chunks .iter() - .map(|chunk| direct_thread_delta_text(root, chunk)) + .map(|chunk| thread_delta_text(root, chunk)) .collect(); let whole = chunks.concat(); assert_eq!(streamed, whole, "逐段脱敏不得吃掉段尾换行"); assert_eq!( - direct_thread_delta_text(root, &whole), + thread_delta_text(root, &whole), streamed, "逐段脱敏与整段脱敏必须同形" ); @@ -6070,7 +6065,7 @@ mod tests { "turn-1" ), Some(CodexTurnEvent::Request { - kind: DirectThreadRequestKind::RequestResolved, + kind: ThreadRequestKind::RequestResolved, .. }) )); @@ -6084,7 +6079,7 @@ mod tests { "turn-1", ), Some(CodexTurnEvent::Request { - kind: DirectThreadRequestKind::ApprovalRequested, + kind: ThreadRequestKind::ApprovalRequested, .. }) )); @@ -6162,7 +6157,7 @@ mod tests { }; let item = event_params.get("item").expect("item payload"); assert_eq!( - direct_thread_item_event_at_ms(&event_params, item, completed, 9_999), + thread_item_event_at_ms(&event_params, item, completed, 9_999), expected_at_ms, "{method} 必须用通知层的阶段时间,而不是宿主钟" ); @@ -6185,7 +6180,7 @@ mod tests { }; let turn = params.get("turn").unwrap_or(¶ms); assert_eq!( - direct_thread_turn_completed_at_ms(turn, Some(1_700_000_000_500), 9_999), + thread_turn_completed_at_ms(turn, Some(1_700_000_000_500), 9_999), 9_999, "上游只有秒级 completedAt:不采用,取宿主处理终态的毫秒钟" ); @@ -6198,7 +6193,7 @@ mod tests { "durationMs": 42_500u64, }); assert_eq!( - direct_thread_turn_completed_at_ms(&with_duration, Some(1_700_000_000_500), 9_999), + thread_turn_completed_at_ms(&with_duration, Some(1_700_000_000_500), 9_999), 1_700_000_043_000, "durationMs + 宿主高精度起点才派生结束" ); @@ -6830,7 +6825,7 @@ mod tests { let stable_link = temp.path().join("current-project"); symlink(&project, &stable_link).expect("project symlink"); - let canonical = direct_thread_id_for_project(&project); + let canonical = thread_id_for_project(&project); assert_eq!( canonical, project @@ -6840,7 +6835,7 @@ mod tests { "线程 id 就是 canonical 路径" ); assert_eq!( - direct_thread_id_for_project(&stable_link), + thread_id_for_project(&stable_link), canonical, "符号链接必须归一到同一个线程 id" ); @@ -6852,7 +6847,7 @@ mod tests { "前提:manifest 读不到时权威身份确实会失败" ); assert_eq!( - direct_thread_id_for_project(&stable_link), + thread_id_for_project(&stable_link), canonical, "manifest 读失败不能把线程 id 退回调用方原始字符串" ); @@ -8181,10 +8176,9 @@ done "id": "direct-codex:turn-0001:user", "content": [{ "type": "input_text", "text": "请创建菜单" }] }); - let thread_id = direct_thread_id_for_project(&project); - let bootstrap = crate::agent::subscribe_direct_thread(&thread_id); - let _reservation = - crate::agent::DirectTurnReservation::accept_for_test(&thread_id, "turn-0001"); + let thread_id = thread_id_for_project(&project); + let bootstrap = crate::agent::subscribe_thread(&thread_id); + let _reservation = crate::agent::TurnReservation::accept_for_test(&thread_id, "turn-0001"); crate::agent::append_direct_project_user_message_at(&project, &user_item) .expect("persist opener user item"); // 生产入口在落盘成功、起 codex 之前就把本轮的用户条目下发(`emit_direct_thread_user_item`): @@ -8242,13 +8236,13 @@ done .await .expect("the turn must finish after the connection is reclaimed"); - let consumed = crate::agent::consume_direct_thread(&bootstrap.subscription_id) - .expect("consume events"); + let consumed = + crate::agent::consume_thread(&bootstrap.subscription_id).expect("consume events"); let terminal = consumed .events .iter() .find_map(|event| match event { - DirectThreadEvent::TurnCompleted { + ThreadEvent::TurnCompleted { status, failure, .. } => Some((status.clone(), failure.clone())), _ => None, @@ -8333,8 +8327,8 @@ done "id": "direct-codex:turn-0001:user", "content": [{"type": "input_text", "text": "请创建菜单"}] }); - let thread_id = direct_thread_id_for_project(&project); - let bootstrap = crate::agent::subscribe_direct_thread(&thread_id); + let thread_id = thread_id_for_project(&project); + let bootstrap = crate::agent::subscribe_thread(&thread_id); assert!( bootstrap.events.is_empty(), "订阅发生在回合之前,bootstrap 必须为空" @@ -8342,8 +8336,7 @@ done // 生产入口(`enqueue_direct_codex_turn` + 放行侧)在起 codex 之前先放行(认领时登记调用身份), // 再把用户条目落盘: // 这里补上同一步,于是这一轮的边界仍在同一个订阅里成对出现,历史里也有那条用户消息。 - let _reservation = - crate::agent::DirectTurnReservation::accept_for_test(&thread_id, "turn-0001"); + let _reservation = crate::agent::TurnReservation::accept_for_test(&thread_id, "turn-0001"); crate::agent::append_direct_project_user_message_at(&project, &user_item) .expect("persist opener user item"); // 生产入口在落盘成功、起 codex 之前就把本轮的用户条目下发(`emit_direct_thread_user_item`): @@ -8382,8 +8375,8 @@ done .expect("run direct-project turn"); drop(observer); - let consumed = crate::agent::consume_direct_thread(&bootstrap.subscription_id) - .expect("consume events"); + let consumed = + crate::agent::consume_thread(&bootstrap.subscription_id).expect("consume events"); // 回合起止必须与开口用户条目同源:前端在「只有锚点 + 历史、运行态为空」的回合里靠这个 // 身份把边界认领给同一条用户条目,缺了它就只能隐藏未知用时。 let lifecycle_user_item_ids = consumed @@ -8392,10 +8385,10 @@ done .filter(|event| { matches!( event, - DirectThreadEvent::TurnStarted { .. } | DirectThreadEvent::TurnCompleted { .. } + ThreadEvent::TurnStarted { .. } | ThreadEvent::TurnCompleted { .. } ) }) - .map(DirectThreadEvent::user_item_id) + .map(ThreadEvent::user_item_id) .collect::>(); assert_eq!( lifecycle_user_item_ids, @@ -8409,12 +8402,13 @@ done let mut assistant_items = Vec::new(); for event in &consumed.events { let item = match event { - DirectThreadEvent::ItemStarted { item, .. } - | DirectThreadEvent::ItemCompleted { item, .. } => item, + ThreadEvent::ItemStarted { item, .. } | ThreadEvent::ItemCompleted { item, .. } => { + item + } _ => continue, }; match item { - DirectThreadItem::Message { + ThreadItem::Message { item_id, role, text, @@ -8422,7 +8416,7 @@ done } if role.as_str() == "user" => { user_items.push((item_id.clone(), text.clone())); } - DirectThreadItem::Message { text, .. } if !text.trim().is_empty() => { + ThreadItem::Message { text, .. } if !text.trim().is_empty() => { assistant_items.push(text.clone()); } _ => {} diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_execution.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_execution.rs index 812c47478..7e279b83e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_execution.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_execution.rs @@ -432,7 +432,7 @@ pub(super) async fn begin( let root = root.to_path_buf(); let prompt_hash = hash(prompt.as_bytes()); let session = tokio::task::spawn_blocking(move || { - let turn = super::direct_active_turn_id_at(&root)?; + let turn = super::active_turn_id_at(&root)?; let host = crate::game_creator_runtime_config_dir() .ok_or("direct-execution-host: 需要客户端私有配置目录,CLI 请提供 --config-dir")?; open_with_analytics_at( diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_context.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_context.rs index 909b786d7..55c58ee63 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_context.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_context.rs @@ -53,7 +53,7 @@ fn context_identity(root: &Path) -> Result { let manifest = read_existing_manifest_for_project(root)?; let revision = read_game_creator_agent_runtime_project_revision(root)?.revision; let canonical = root.canonicalize().map_err(|_| "项目上下文目录无法解析")?; - let active = list_direct_active_turns()?.into_iter().find(|turn| { + let active = list_active_turns()?.into_iter().find(|turn| { Path::new(&turn.project_path).canonicalize().ok().as_ref() == Some(&canonical) }); Ok(ContextIdentity { @@ -506,7 +506,7 @@ pub(super) async fn prefetch_turn_input( let scan_root = root.to_path_buf(); let expected_turn = client_turn_id.to_string(); let files = tokio::task::spawn_blocking(move || { - if direct_active_turn_id_at(&scan_root).ok().as_deref() != Some(expected_turn.as_str()) { + if active_turn_id_at(&scan_root).ok().as_deref() != Some(expected_turn.as_str()) { return Vec::new(); } let candidates = [ @@ -621,10 +621,7 @@ mod tests { std::fs::write(root.join("code.js"), "unchanged").unwrap(); // 身份来自逻辑回合(Thread Manager):放行才是"这一轮在跑"的唯一登记。 let owner = Arc::new(std::sync::Mutex::new(Some( - DirectTurnReservation::accept_for_test( - &direct_thread_id_for_project(&root), - "turn-before", - ), + TurnReservation::accept_for_test(&thread_id_for_project(&root), "turn-before"), ))); let swap = Arc::clone(&owner); let result = read_batch_with( @@ -635,8 +632,8 @@ mod tests { let result = read_file(r, f, b); let mut guard = swap.lock().unwrap(); drop(guard.take()); - *guard = Some(DirectTurnReservation::accept_for_test( - &direct_thread_id_for_project(r), + *guard = Some(TurnReservation::accept_for_test( + &thread_id_for_project(r), "turn-after", )); result @@ -802,10 +799,8 @@ mod tests { async fn host_prefetch_keeps_data_out_of_system_rules_and_matches_active_turn() { let (_temp, root) = project(); // 预取闸门与上下文身份读的是同一处:Thread Manager 上这一轮的活动回合登记。 - let _turn = DirectTurnReservation::accept_for_test( - &direct_thread_id_for_project(&root), - "prefetch-turn", - ); + let _turn = + TurnReservation::accept_for_test(&thread_id_for_project(&root), "prefetch-turn"); let data = prefetch_turn_input(&root, "prefetch-turn") .await .unwrap() diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_history.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_history.rs index f1e6679bb..e7399982a 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_history.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_history.rs @@ -1,5 +1,5 @@ -use super::direct_thread_item_identity; use super::runtime_actions::acquire_game_creator_agent_runtime_project_write_lock_with_wait; +use super::thread_item_identity; use crate::config::prepare_game_creator_private_path_for_read; use crate::project::{ append_jsonl_line_unlocked, enforce_project_permission_policy, project_append_lock_for, @@ -610,7 +610,7 @@ fn direct_project_history_anchor_id(item: &Value) -> Option { .map(str::trim) .filter(|value| !value.is_empty()) .map(str::to_string) - .or_else(|| direct_thread_item_identity(item)) + .or_else(|| thread_item_identity(item)) } /// 一屏历史窗口在新端(较新一侧)的锚点。 @@ -719,7 +719,7 @@ pub(crate) fn read_direct_project_history_items_slice_at( let recorded_at_ms = newest_first .iter() .filter_map(|(item, at)| { - let identity = direct_thread_item_identity(item)?; + let identity = thread_item_identity(item)?; (*at > 0).then_some((identity, *at)) }) .collect(); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs index e375a3867..b537eeac9 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs @@ -458,8 +458,8 @@ fn direct_taonier_regeneration_invocation_sha256(invocation_id: &str) -> String /// /// 拿不到身份时返回带前缀的错误:调用方(付费美术执行会话、工具桥、校验预约、上下文预取)都靠 /// "拿不到身份"来拒绝"在没有回合的情况下创建 / 清理生成账本"。 -pub(crate) fn direct_active_turn_id_at(root: &Path) -> Result { - read_direct_turn_identity(&direct_thread_id_for_project(root)) +pub(crate) fn active_turn_id_at(root: &Path) -> Result { + read_turn_identity(&thread_id_for_project(root)) .map(|identity| identity.client_turn_id) .ok_or_else(|| { format!( @@ -472,7 +472,7 @@ pub(crate) fn direct_active_turn_id_at(root: &Path) -> Result { /// /// 上限用来挡"刚放行、还在本地准备(读配置 / manifest / 拼系统提示 / 开审计)的回合被终止误伤": /// 这期间执行器侧还没有这一轮的登记,但这一轮马上就会进去,提前释放等于放开并发。 -const DIRECT_STALE_TURN_RELEASE_MIN_AGE_MS: u64 = 60_000; +const STALE_TURN_RELEASE_MIN_AGE_MS: u64 = 60_000; fn direct_taonier_active_now_millis() -> u64 { unix_millis().min(u128::from(u64::MAX)) as u64 @@ -480,14 +480,14 @@ fn direct_taonier_active_now_millis() -> u64 { /// "终止"拿不到可中断句柄时的分类,决定这一轮能不能被兜底释放。 #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) enum DirectStaleTurnReleaseReason { +pub(crate) enum StaleTurnReleaseReason { /// app-server 侧登记着这一轮,但执行进程已经退出:这一轮不可能再有收尾。 ExecutorExited, /// app-server 侧完全没有这一轮的登记:只有过了正常启动窗口才允许释放。 NeverReachedExecutor, } -impl DirectStaleTurnReleaseReason { +impl StaleTurnReleaseReason { pub(crate) fn message(self) -> &'static str { match self { Self::ExecutorExited => "陶泥儿执行进程已退出", @@ -503,31 +503,31 @@ impl DirectStaleTurnReleaseReason { /// 1. 这个 thread 上确实登记着一轮(Thread Manager 的活动回合); /// 2. 传了 `expected_client_turn_id` 时必须与登记一致——绝不误伤另一条回合; /// 3. `reason == NeverReachedExecutor` 时,登记的年龄必须超过 -/// [`DIRECT_STALE_TURN_RELEASE_MIN_AGE_MS`],排除"刚放行、还在本地准备"的正常启动窗口。 +/// [`STALE_TURN_RELEASE_MIN_AGE_MS`],排除"刚放行、还在本地准备"的正常启动窗口。 /// /// 不满足任何一条都**什么都不写**(占用、事件都不变),返回可读原因;因此它天然幂等——占用已经 -/// 没了就返回"没有正在运行的回合"。这一层只负责把 [`DirectStaleTurnRelease`] 翻成人话:判据与释放 +/// 没了就返回"没有正在运行的回合"。这一层只负责把 [`StaleTurnRelease`] 翻成人话:判据与释放 /// 动作都在 Thread Manager 里,因为它们必须在同一个临界区里(拆成两步会让并发认领插进中间,见 /// [`release_stale_direct_turn`])。 -pub(crate) fn direct_stale_turn_for_release( +pub(crate) fn stale_turn_for_release( root: &Path, expected_client_turn_id: Option<&str>, - reason: DirectStaleTurnReleaseReason, + reason: StaleTurnReleaseReason, ) -> Result { let expected = expected_client_turn_id .map(str::trim) .filter(|value| !value.is_empty()); - let min_age_ms = (reason == DirectStaleTurnReleaseReason::NeverReachedExecutor) - .then_some(DIRECT_STALE_TURN_RELEASE_MIN_AGE_MS); - match release_stale_direct_turn(&direct_thread_id_for_project(root), expected, min_age_ms) { - DirectStaleTurnRelease::Released(client_turn_id) => Ok(client_turn_id), - DirectStaleTurnRelease::NoActiveTurn => { + let min_age_ms = (reason == StaleTurnReleaseReason::NeverReachedExecutor) + .then_some(STALE_TURN_RELEASE_MIN_AGE_MS); + match release_stale_direct_turn(&thread_id_for_project(root), expected, min_age_ms) { + StaleTurnRelease::Released(client_turn_id) => Ok(client_turn_id), + StaleTurnRelease::NoActiveTurn => { Err("当前项目没有正在运行的陶泥儿回合,无法终止".to_string()) } - DirectStaleTurnRelease::AnotherTurnRunning => { + StaleTurnRelease::AnotherTurnRunning => { Err("正在运行的是另一条 Direct 客户端回合,已拒绝终止".to_string()) } - DirectStaleTurnRelease::WithinStartupWindow { age_ms } => Err(format!( + StaleTurnRelease::WithinStartupWindow { age_ms } => Err(format!( "这一轮 Direct 客户端回合刚开始 {} 秒、还在准备中,暂不能强制释放;请稍后再试", age_ms / 1000 )), @@ -3159,7 +3159,7 @@ pub(crate) async fn ensure_direct_taonier_art_package_at( )); } let mut regeneration_workflow = if mode.regenerates_existing() { - let invocation_id = direct_active_turn_id_at(root)?; + let invocation_id = active_turn_id_at(root)?; Some(prepare_direct_taonier_regeneration_workflow_at( root, prompt, @@ -5524,23 +5524,23 @@ mod tests { #[test] fn reading_the_active_turn_identity_does_not_take_over_the_occupancy() { let root = tempfile::tempdir().expect("active turn root"); - let thread_id = direct_thread_id_for_project(root.path()); - assert_eq!(read_direct_turn_identity(&thread_id), None); + let thread_id = thread_id_for_project(root.path()); + assert_eq!(read_turn_identity(&thread_id), None); - let reservation = DirectTurnReservation::accept_for_test(&thread_id, "client-turn-read-1"); - let running = read_direct_turn_identity(&thread_id).expect("running turn is readable"); + let reservation = TurnReservation::accept_for_test(&thread_id, "client-turn-read-1"); + let running = read_turn_identity(&thread_id).expect("running turn is readable"); assert_eq!(running.client_turn_id, "client-turn-read-1"); assert!(running.started_at > 0, "{running:?}"); assert_eq!( - direct_active_turn_id_at(root.path()).expect("identity for the running turn"), + active_turn_id_at(root.path()).expect("identity for the running turn"), "client-turn-read-1" ); // 只读探测不释放占用:认领仍然被这一轮挡着。 - assert!(claim_direct_pending_turn(&thread_id).is_none()); + assert!(claim_pending_turn(&thread_id).is_none()); drop(reservation); - assert_eq!(read_direct_turn_identity(&thread_id), None); - assert!(direct_active_turn_id_at(root.path()).is_err()); + assert_eq!(read_turn_identity(&thread_id), None); + assert!(active_turn_id_at(root.path()).is_err()); } /// 兜底释放的前置判据:身份对得上,且"从没进执行器"必须过启动窗口;判据不过**一个字都不写**, @@ -5548,36 +5548,36 @@ mod tests { #[test] fn stale_turn_release_requires_a_matching_identity_and_only_after_the_start_window() { let root = tempfile::tempdir().expect("stale turn root"); - let thread_id = direct_thread_id_for_project(root.path()); - let reservation = DirectTurnReservation::accept_for_test(&thread_id, "client-turn-stale-1"); - let watcher = subscribe_direct_thread(&thread_id); - let _ = consume_direct_thread(&watcher.subscription_id).expect("drain bootstrap"); + let thread_id = thread_id_for_project(root.path()); + let reservation = TurnReservation::accept_for_test(&thread_id, "client-turn-stale-1"); + let watcher = subscribe_thread(&thread_id); + let _ = consume_thread(&watcher.subscription_id).expect("drain bootstrap"); // ① clientTurnId 不匹配:拒绝,且不误伤正在跑的那一轮。 - let mismatch = direct_stale_turn_for_release( + let mismatch = stale_turn_for_release( root.path(), Some("client-turn-stale-2"), - DirectStaleTurnReleaseReason::ExecutorExited, + StaleTurnReleaseReason::ExecutorExited, ) .expect_err("another turn must not be released"); assert!(mismatch.contains("另一条"), "{mismatch}"); // ② 刚放行、还没进执行器:正常启动窗口内不许释放(释放等于放开并发)。 - let young = direct_stale_turn_for_release( + let young = stale_turn_for_release( root.path(), Some("client-turn-stale-1"), - DirectStaleTurnReleaseReason::NeverReachedExecutor, + StaleTurnReleaseReason::NeverReachedExecutor, ) .expect_err("a freshly dispatched turn is still starting"); assert!(young.contains("暂不能强制释放"), "{young}"); // ① ② 两条拒绝路径都**什么都没写**:占用还在,事件流上也没有任何东西。 assert_eq!( - read_direct_turn_identity(&thread_id).map(|turn| turn.client_turn_id), + read_turn_identity(&thread_id).map(|turn| turn.client_turn_id), Some("client-turn-stale-1".to_string()) ); assert!( - consume_direct_thread(&watcher.subscription_id) + consume_thread(&watcher.subscription_id) .expect("consume after rejected releases") .events .is_empty(), @@ -5585,20 +5585,16 @@ mod tests { ); // ③ 老过窗口:判成残留——占用与兜底终态在同一个临界区里一起落地。 - backdate_direct_active_turn_for_test(&thread_id, DIRECT_STALE_TURN_RELEASE_MIN_AGE_MS + 1); - let released = direct_stale_turn_for_release( + backdate_direct_active_turn_for_test(&thread_id, STALE_TURN_RELEASE_MIN_AGE_MS + 1); + let released = stale_turn_for_release( root.path(), Some("client-turn-stale-1"), - DirectStaleTurnReleaseReason::NeverReachedExecutor, + StaleTurnReleaseReason::NeverReachedExecutor, ) .expect("stale turn is releasable"); assert_eq!(released, "client-turn-stale-1"); - assert_eq!( - read_direct_turn_identity(&thread_id), - None, - "占用必须一起解除" - ); - let events = consume_direct_thread(&watcher.subscription_id) + assert_eq!(read_turn_identity(&thread_id), None, "占用必须一起解除"); + let events = consume_thread(&watcher.subscription_id) .expect("consume released terminal") .events; assert_eq!(events.len(), 1, "兜底释放只写一条终态:{events:?}"); @@ -5606,7 +5602,7 @@ mod tests { assert!( matches!( terminal, - DirectThreadEvent::TurnCompleted { status, .. } if status == "aborted" + ThreadEvent::TurnCompleted { status, .. } if status == "aborted" ), "{terminal:?}" ); @@ -5619,14 +5615,10 @@ mod tests { // ④ 释放之后这个 thread 立刻腾出来了(占用解除与终态同批落地):下一条能重新认领, // 而且"执行进程已退出"不看启动窗口——刚放行也允许释放。 - let _next = DirectTurnReservation::accept_for_test(&thread_id, "client-turn-stale-2"); + let _next = TurnReservation::accept_for_test(&thread_id, "client-turn-stale-2"); assert_eq!( - direct_stale_turn_for_release( - root.path(), - None, - DirectStaleTurnReleaseReason::ExecutorExited, - ) - .expect("executor exited ignores the start window"), + stale_turn_for_release(root.path(), None, StaleTurnReleaseReason::ExecutorExited,) + .expect("executor exited ignores the start window"), "client-turn-stale-2" ); drop(reservation); @@ -5715,12 +5707,9 @@ mod tests { #[test] fn stale_turn_release_without_an_active_turn_says_so() { let root = tempfile::tempdir().expect("empty turn root"); - let nothing = direct_stale_turn_for_release( - root.path(), - None, - DirectStaleTurnReleaseReason::ExecutorExited, - ) - .expect_err("nothing to release"); + let nothing = + stale_turn_for_release(root.path(), None, StaleTurnReleaseReason::ExecutorExited) + .expect_err("nothing to release"); assert!(nothing.contains("没有正在运行"), "{nothing}"); } @@ -6701,8 +6690,8 @@ mod tests { .expect("completed replay authority is the stable outer turn, not a resampled brief"); assert_eq!(completed_replay, persisted); - let _active_turn = DirectTurnReservation::accept_for_test( - &direct_thread_id_for_project(root.path()), + let _active_turn = TurnReservation::accept_for_test( + &thread_id_for_project(root.path()), "client-turn-completed-1", ); let replay = ensure_direct_taonier_art_package_at( diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/user_input.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/user_input.rs index d52806b46..b2f2354ce 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/user_input.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/user_input.rs @@ -1,7 +1,7 @@ //! DirectProject 用户输入命令适配器。 //! //! Tauri 只在这里接收前端 item,校验与 canonical→prompt 投影交给 user-item 深模块,队列交给 -//! Thread Manager;放行在 `direct_turn_dispatch`,不在这里。 +//! Thread Manager;放行在 `thread_manager::dispatch`,不在这里。 use super::*; @@ -35,7 +35,7 @@ pub(crate) fn normalize_direct_client_turn_id( /// 之后只做一件事——把待发消息排进队列:不登记占用、不落盘、不发回合事件、不起 codex。 /// /// 于是"这一轮跑成什么"仍然只有订阅事件一个来源:命令返回 `Ok` 只说明**入队成立**。真正的回合边界 -/// (`turn.started` / `turn.completed`)由 Thread Manager 在**放行**时写出(见 `direct_turn_dispatch`), +/// (`turn.started` / `turn.completed`)由 Thread Manager 在**放行**时写出(见 `thread_manager::dispatch`), /// 入队失败不写用户条目、不产生任何事件。可留痕的调用级失败(宿主 / 环境事实)仍在边界补一份运行 /// 错误诊断,返回串不带诊断引用。 /// @@ -93,15 +93,13 @@ async fn enqueue_direct_codex_turn_typed( let user_prompt = direct_codex_user_item_to_prompt(root, &user_item) .map_err(|detail| DirectTurnError::InputRejected { detail })?; check_direct_turn_preconditions(root, &user_prompt, creation_type.as_deref())?; - let thread_id = direct_thread_id_for_project(root); + let thread_id = thread_id_for_project(root); // 容量是**最便宜**的一条入队前置条件,先判:满了就直接说满,别让一条注定收不下的消息先去做工程 // 准备——那是分钟级的活(`npm ci` + Vite 构建),还会在磁盘上留下产物。权威判据仍然是入队那一刻 - // 临界区里的容量检查(下面 `enqueue_direct_pending_turn`):这里只是快速失败,中间被别人的消息 + // 临界区里的容量检查(下面 `enqueue_pending_turn`):这里只是快速失败,中间被别人的消息 // 挤满时那一条照样拦得住。 - queue_has_room(direct_pending_turn_count(&thread_id)).map_err(|_| { - DirectTurnError::QueueFull { - limit: MAX_PENDING_DIRECT_TURNS, - } + queue_has_room(pending_turn_count(&thread_id)).map_err(|_| DirectTurnError::QueueFull { + limit: MAX_PENDING_TURNS, })?; // 创建类型来自结构化用户入口;实际工程和可信脚手架由宿主复核。 crate::environment_check::prepare_new_web_project_at(root, creation_type.as_deref()) @@ -112,7 +110,7 @@ async fn enqueue_direct_codex_turn_typed( })?; // 入队:到这里这一条已经过了全部检查,剩下的就是排队等放行。prompt 与 canonical 形状在这里 // 冻结——放行不重算,所以放行没有失败出口。 - let pending = PendingDirectTurn::prepare( + let pending = PendingTurn::prepare( turn_id, user_item, user_prompt, @@ -123,37 +121,37 @@ async fn enqueue_direct_codex_turn_typed( .map_err(|error| DirectTurnError::InputRejected { detail: error.to_string(), })?; - match enqueue_direct_pending_turn(&thread_id, pending) { + match enqueue_pending_turn(&thread_id, pending) { Ok(_) => {} Err(EnqueueRejection::QueueFull) => { return Err(DirectTurnError::QueueFull { - limit: MAX_PENDING_DIRECT_TURNS, + limit: MAX_PENDING_TURNS, }) } } // 入队之后立刻踢一脚:队列空且没有回合在跑时,放行就是这一脚,用户点发送不必再等一个调度周期。 - kick_direct_queue_dispatch(root); + kick_queue_dispatch(root); Ok(()) } #[cfg(test)] mod tests { use super::*; - use crate::agent::{consume_direct_thread, subscribe_direct_thread, DirectThreadEvent}; + use crate::agent::{consume_thread, subscribe_thread, ThreadEvent}; /// 放行之后整轮是异步的:测试只能等事件。返回从订阅之后看到的全部事件。 - async fn wait_for_completion(subscription_id: &str) -> Vec { + async fn wait_for_completion(subscription_id: &str) -> Vec { let mut events = Vec::new(); let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30); loop { events.extend( - consume_direct_thread(subscription_id) + consume_thread(subscription_id) .expect("consume logical turn") .events, ); if events .iter() - .any(|event| matches!(event, DirectThreadEvent::TurnCompleted { .. })) + .any(|event| matches!(event, ThreadEvent::TurnCompleted { .. })) { return events; } @@ -184,9 +182,9 @@ mod tests { let temp = tempfile::tempdir().expect("temp dir"); let root = temp.path().join("not-a-project"); std::fs::create_dir_all(&root).expect("create project dir"); - let thread_id = direct_thread_id_for_project(&root); - let subscription = subscribe_direct_thread(&thread_id); - let _ = consume_direct_thread(&subscription.subscription_id); + let thread_id = thread_id_for_project(&root); + let subscription = subscribe_thread(&thread_id); + let _ = consume_thread(&subscription.subscription_id); enqueue_direct_codex_turn_typed( &root, @@ -202,7 +200,7 @@ mod tests { let terminal = events .iter() .filter_map(|event| match event { - DirectThreadEvent::TurnCompleted { + ThreadEvent::TurnCompleted { status, failure, user_item_id, @@ -221,7 +219,7 @@ mod tests { ); assert_eq!(user_item_id.as_deref(), Some("direct-codex:turn-1:user")); // 占用已释放:队列继续走得动。 - assert!(!crate::agent::direct_thread_turn_is_active(&thread_id)); + assert!(!crate::agent::thread_turn_is_active(&thread_id)); } /// 本轮的开口用户条目必须先于整轮里任何可能失败的东西下发。 @@ -239,9 +237,9 @@ mod tests { let root = temp.path().join("direct-user-item-first"); crate::init_local_game_project_at(&root, "direct-user-item-first", "用户条目先下发") .expect("init project"); - let thread_id = direct_thread_id_for_project(&root); - let subscription = subscribe_direct_thread(&thread_id); - let _ = consume_direct_thread(&subscription.subscription_id); + let thread_id = thread_id_for_project(&root); + let subscription = subscribe_thread(&thread_id); + let _ = consume_thread(&subscription.subscription_id); enqueue_direct_codex_turn_typed( &root, @@ -257,12 +255,12 @@ mod tests { // 队列事件先于放行:`queue.enqueued` → 放行(开始 + 离开队列)→ 开口用户条目 → 终态。 let started = events .iter() - .position(|event| matches!(event, DirectThreadEvent::TurnStarted { .. })) + .position(|event| matches!(event, ThreadEvent::TurnStarted { .. })) .expect("这一轮必须被放行"); assert!( matches!( events.get(started), - Some(DirectThreadEvent::TurnStarted { user_item_id, .. }) + Some(ThreadEvent::TurnStarted { user_item_id, .. }) if user_item_id.as_deref() == Some("direct-codex:turn-1:user") ), "放行的第一条事件必须是带身份的回合开始:{events:?}" @@ -275,7 +273,7 @@ mod tests { assert!( matches!( events.get(opening), - Some(DirectThreadEvent::ItemCompleted { item, .. }) + Some(ThreadEvent::ItemCompleted { item, .. }) if item.item_id() == "direct-codex:turn-1:user" ), "第一条条目事件必须是本轮的开口用户条目:{events:?}" @@ -283,7 +281,7 @@ mod tests { assert!(opening > started, "用户条目只能在放行之后:{events:?}"); let terminal = events .iter() - .position(|event| matches!(event, DirectThreadEvent::TurnCompleted { .. })); + .position(|event| matches!(event, ThreadEvent::TurnCompleted { .. })); assert!( terminal.is_none_or(|index| index > opening), "终态只能在用户条目之后:{events:?}" @@ -310,9 +308,9 @@ mod tests { let root = temp.path().join("direct-history-write-failure"); crate::init_local_game_project_at(&root, "direct-history-write", "落盘失败") .expect("init project"); - let thread_id = direct_thread_id_for_project(&root); - let subscription = subscribe_direct_thread(&thread_id); - let _ = consume_direct_thread(&subscription.subscription_id); + let thread_id = thread_id_for_project(&root); + let subscription = subscribe_thread(&thread_id); + let _ = consume_thread(&subscription.subscription_id); // 接下来这次追加写的两次尝试都按"争用失败"返回:确定性地走到落盘失败分支。 std::fs::write( root.join(".agent/runtime/test-fail-next-direct-project-history-append"), @@ -334,7 +332,7 @@ mod tests { let terminals = events .iter() .filter_map(|event| match event { - DirectThreadEvent::TurnCompleted { + ThreadEvent::TurnCompleted { status, failure, .. } => Some((status, failure)), _ => None, @@ -357,6 +355,6 @@ mod tests { failure.message ); // 占用已释放:下一轮还能继续。 - assert!(!crate::agent::direct_thread_turn_is_active(&thread_id)); + assert!(!crate::agent::thread_turn_is_active(&thread_id)); } } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs index e6566b84d..27af35cbd 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs @@ -267,7 +267,7 @@ impl DirectToolBridge { impl DirectToolBridgeState { fn begin_user_turn(self: &Arc) -> Result { - let turn_id = direct_active_turn_id_at(&self.root)?; + let turn_id = active_turn_id_at(&self.root)?; let mut authorization = self .turn_authorization .lock() @@ -4839,10 +4839,8 @@ mod tests { let state = direct_tool_bridge_state(root.path().to_path_buf()); assert!(state.begin_user_turn().is_err()); let client_turn_id = "client-turn-stable-0001"; - let _active_invocation = DirectTurnReservation::accept_for_test( - &direct_thread_id_for_project(root.path()), - client_turn_id, - ); + let _active_invocation = + TurnReservation::accept_for_test(&thread_id_for_project(root.path()), client_turn_id); let active_turn = state .begin_user_turn() .expect("client turn authorization state"); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_calls.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_calls.rs index 718ed37a8..9d83b435d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_calls.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_calls.rs @@ -8,7 +8,7 @@ //! 为什么不复用 `project.jsonl`:那条链路的回读只投影 `role ∈ {user, assistant}` 的 //! 文本条目,而且会被注入 Codex 上下文。往里面塞新形状既装不下,又有污染模型上下文的风险。 -use super::direct_thread_wire::sanitize_detail_text; +use super::thread_manager::wire::sanitize_detail_text; use crate::config::write_game_creator_private_file; use crate::project::{enforce_project_permission_policy, project_append_lock_for}; use serde::{Deserialize, Serialize}; diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs index 4e5e08a23..5deb4efc2 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs @@ -3850,16 +3850,14 @@ mod tests { root: &Path, ) -> ( super::super::direct_tool_bridge::DirectToolBridge, - DirectTurnReservation, + TurnReservation, super::super::direct_tool_bridge::DirectExecutionTestFixture, ) { let bridge = super::super::direct_tool_bridge::start_direct_tool_bridge(root, false) .await .expect("start tool chain bridge"); - let turn = DirectTurnReservation::accept_for_test( - &direct_thread_id_for_project(root), - "tool-chain-turn", - ); + let turn = + TurnReservation::accept_for_test(&thread_id_for_project(root), "tool-chain-turn"); let execution = super::super::direct_tool_bridge::direct_execution_fixture(root, "tool-chain-turn") .await; diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_turn_error.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_turn_error.rs index 80144ee12..39cbfbb72 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_turn_error.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_turn_error.rs @@ -14,7 +14,7 @@ //! 所以"同一种错误在入队与放行走不同通道"是正常的:[`DirectTurnError::EnvironmentNotReady`] 两边 //! 都可能出现,位置说了算。这里**没有**、也不该有"这个变体是不是回合失败"的判据。 //! -//! 事件载荷(`direct_thread_wire::DirectTurnFailure`)仍然只有 `{kind, message}` 两个字段: +//! 事件载荷(`thread_manager::wire::DirectTurnFailure`)仍然只有 `{kind, message}` 两个字段: //! 那是**线上协议**,由 [`DirectTurnError::wire_kind`] 与 `Display` 在这一个出口投影出来,不是 //! 另一种状态模型。跨进程边界(`#[tauri::command]`)同样只给前端一个字符串:那是**序列化**, //! 由 `Display` 一处生成;Rust 侧任何地方都不再解析这个字符串。 @@ -392,7 +392,7 @@ pub(crate) enum DirectTurnError { ClientTurnIdMalformed { min_chars: usize, max_chars: usize }, /// 待发消息队列已满:这一条没进队,等前面几条发完再发。 /// - /// 上限只落在宿主这一处(`MAX_PENDING_DIRECT_TURNS`),随载荷带出去,界面不自己数一份。 + /// 上限只落在宿主这一处(`MAX_PENDING_TURNS`),随载荷带出去,界面不自己数一份。 QueueFull { limit: usize }, /// 项目目录锚不定(符号链接 / 权限 / 目录被删)。 ProjectRootUnanchored { cause: String }, diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_turn_failure.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_turn_failure.rs index 5b01d1f9f..1d3d004c9 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_turn_failure.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_turn_failure.rs @@ -5,10 +5,10 @@ //! 1. [`direct_turn_terminal`]:拿这一轮的事实判定终态——是不是失败、原因是什么、状态写什么; //! 2. [`DirectTurnTerminal::event`]:把终态投影成 `turn.completed` 事件。 //! -//! 终态的**出口**(谁写、什么时候兜底)不在这里,在 `direct_turn_dispatch.rs` 的放行占用对象里: +//! 终态的**出口**(谁写、什么时候兜底)不在这里,在 `thread_manager::dispatch` 的放行占用对象里: //! 这个模块只负责"什么算失败、原因怎么写"。 //! -//! 失败载荷的**形状**属于线上协议,定义在 `direct_thread_wire.rs`(`DirectTurnFailure`); +//! 失败载荷的**形状**属于线上协议,定义在 `thread_manager::wire`(`DirectTurnFailure`); //! 载荷的 `kind` 与 `message` 由 [`DirectTurnError`] 投影而来(`kind` 的取值表见 //! [`DirectTurnError::wire_kind`]);这里只负责"什么算失败、原因怎么写、什么时候兜底", //! 不碰事件队列的搬运规则,也不自己认 `LlmError`。 @@ -16,8 +16,8 @@ use std::path::Path; use super::{ - redact_agent_runtime_error, DirectThreadEvent, DirectTurnError, DirectTurnFailure, - DirectTurnFailureKind, + redact_agent_runtime_error, DirectTurnError, DirectTurnFailure, DirectTurnFailureKind, + ThreadEvent, }; /// `turn.completed.failure.message` 的字符上限:与本地错误文案同一档——够说清原因,又不至于 @@ -36,10 +36,10 @@ pub(crate) struct DirectTurnTerminal { impl DirectTurnTerminal { /// 终态事件:失败时同一个 `turn.completed` 带载荷,其余只带 `status`。 - pub(crate) fn event(self, completed_at: u64, user_item_id: Option<&str>) -> DirectThreadEvent { + pub(crate) fn event(self, completed_at: u64, user_item_id: Option<&str>) -> ThreadEvent { let event = match self.failure { - Some(failure) => DirectThreadEvent::turn_completed_failed(failure, completed_at), - None => DirectThreadEvent::turn_completed(self.status, completed_at), + Some(failure) => ThreadEvent::turn_completed_failed(failure, completed_at), + None => ThreadEvent::turn_completed(self.status, completed_at), }; event.with_user_item_id(user_item_id) } @@ -122,7 +122,7 @@ impl DirectTurnTerminal { #[cfg(test)] mod tests { use super::*; - use crate::agent::{consume_direct_thread, subscribe_direct_thread}; + use crate::agent::{consume_thread, subscribe_thread}; use platform_llm::LlmError; fn history_root() -> std::path::PathBuf { @@ -245,7 +245,7 @@ stderrClass=nonempty;stderrBytes=1000"; assert!(event.failure().is_none()); assert!(matches!( event, - DirectThreadEvent::TurnCompleted { ref status, .. } if status == "completed" + ThreadEvent::TurnCompleted { ref status, .. } if status == "completed" )); } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_validation.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_validation.rs index 2cffc419b..4dc25f47f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_validation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_validation.rs @@ -368,10 +368,10 @@ async fn start_for_current_turn( key: String, build: bool, ) -> Result { - let turn_id = direct_active_turn_id_at(root)?; + let turn_id = active_turn_id_at(root)?; let root = root.to_path_buf(); tokio::task::spawn_blocking(move || { - if direct_active_turn_id_at(&root)? != turn_id { + if active_turn_id_at(&root)? != turn_id { return Err("validation-turn-changed: 当前验证所属回合已结束".into()); } let session = super::direct_execution::current(&root)?; diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/prompt_context.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/prompt_context.rs index 5aa5afb51..1bffed9d3 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/prompt_context.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/prompt_context.rs @@ -625,7 +625,7 @@ const ERROR_REDACTED_KEY: &str = "[redacted-sensitive-field]"; /// information whenever a safe error line mentions a credential field. /// /// 逐行脱敏,但**保留每个 chunk 末尾的换行**:本函数会被流式增量逐段调用 -/// (`direct_thread_delta_text` → 前端把各段拼成一条消息再交给 Markdown 渲染)。用 +/// (`thread_delta_text` → 前端把各段拼成一条消息再交给 Markdown 渲染)。用 /// `lines()` + `join("\n")` 会把「以换行结尾的段」的末尾换行吃掉,拼接后段落、列表项和表格行 /// 会并进同一行,整条消息的 Markdown 结构(尤其表格)就作废了。 pub(crate) fn sanitize_error_context(value: &str) -> String { diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/entrypoints.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/entrypoints.rs index 1628f6b0b..10dd259f3 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/entrypoints.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/entrypoints.rs @@ -71,7 +71,7 @@ impl DirectGameCreatorTurnUpdateEmitter { pub(crate) fn new(root: &Path, turn_id: String) -> Self { Self { project_path: root.to_string_lossy().into_owned(), - thread_id: crate::agent::direct_thread_id_for_project(root), + thread_id: crate::agent::thread_id_for_project(root), turn_id, sequence: Arc::new(AtomicU64::new(0)), } @@ -168,7 +168,7 @@ impl DirectGameCreatorTurnUpdateEmitter { .unwrap_or_default() .as_millis() .min(u64::MAX as u128) as u64; - update_direct_thread_active_turn( + update_active_turn( &self.thread_id, &self.turn_id, status, diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_turn_dispatch.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs similarity index 76% rename from apps/ai-game-creator-shell/src-tauri/src/agent/direct_turn_dispatch.rs rename to apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs index 79c8af159..9d9f7b7fb 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_turn_dispatch.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs @@ -1,8 +1,8 @@ //! DirectProject 的放行:把队首那条待发消息变成一轮真的回合。 //! //! 这个模块两件事,别再往里加第三件: -//! 1. [`DirectTurnReservation`]:这一轮的占用对象——持有它代表还没收口,终态出口只认它的 token; -//! 2. [`kick_direct_queue_dispatch`]:认领队首 → 起整轮(落盘用户条目 → 下发 → 跑回合)。 +//! 1. [`TurnReservation`]:这一轮的占用对象——持有它代表还没收口,终态出口只认它的 token; +//! 2. [`kick_queue_dispatch`]:认领队首 → 起整轮(落盘用户条目 → 下发 → 跑回合)。 //! //! 这一轮的**调用身份**(付费美术、执行会话、MCP、校验、上下文预取读的那个"这一轮是谁")由认领 //! 在 Thread Manager 上登记,认领成功即成立;这里不再另取一次、也不再持第二份身份。 @@ -13,19 +13,19 @@ use std::path::{Path, PathBuf}; -use super::{ - append_direct_project_user_message_at, claim_direct_pending_turn, - complete_direct_thread_turn_if_reserved, direct_thread_id_for_project, direct_tool_call_now_ms, - redact_agent_runtime_error, run_direct_game_creator_turn_at_with_creation_type_and_emitter, - DirectDispatchedTurn, DirectGameCreatorTurnUpdateEmitter, DirectTurnError, DirectTurnTerminal, - PendingDirectTurn, +use crate::agent::{ + append_direct_project_user_message_at, claim_pending_turn, complete_turn_if_reserved, + direct_tool_call_now_ms, redact_agent_runtime_error, + run_direct_game_creator_turn_at_with_creation_type_and_emitter, thread_id_for_project, + DirectGameCreatorTurnUpdateEmitter, DirectTurnError, DirectTurnTerminal, DispatchedTurn, + PendingTurn, }; /// 一次放行的占用。持有它就代表这一轮还没收口。 /// -/// 生命周期由调用方决定:登记与开始事件已经由 [`kick_direct_queue_dispatch`] 在同一临界区里写好, +/// 生命周期由调用方决定:登记与开始事件已经由 [`kick_queue_dispatch`] 在同一临界区里写好, /// 这里只接住这一轮的终态出口,并随放行任务一起 drop(正常 / 失败 / panic 都走同一个出口)。 -pub(crate) struct DirectTurnReservation { +pub(crate) struct TurnReservation { thread_id: String, token: String, user_item_id: Option, @@ -36,9 +36,9 @@ pub(crate) struct DirectTurnReservation { _session_keepalive: Option, } -impl DirectTurnReservation { +impl TurnReservation { /// 接管一次放行的占用:占用登记与 `turn.started` 已由 Thread Manager 的认领写好。 - pub(crate) fn resume(thread_id: &str, dispatched: &DirectDispatchedTurn) -> Self { + pub(crate) fn resume(thread_id: &str, dispatched: &DispatchedTurn) -> Self { Self { thread_id: thread_id.to_string(), token: dispatched.token.clone(), @@ -51,7 +51,7 @@ impl DirectTurnReservation { /// /// 深层(真正跑完这一轮的代码)已经写出终态时返回 `false`,兜底不覆盖真实结果。 pub(crate) fn finish_if_unfinished(&self, terminal: DirectTurnTerminal) -> bool { - complete_direct_thread_turn_if_reserved( + complete_turn_if_reserved( &self.thread_id, &self.token, terminal.event(direct_tool_call_now_ms(), self.user_item_id.as_deref()), @@ -61,7 +61,7 @@ impl DirectTurnReservation { /// 测试用:直接开一轮占用,等价于"入队 + 认领",不经过 Tauri 命令与真实的用户条目。 #[cfg(test)] pub(crate) fn accept_for_test(thread_id: &str, client_turn_id: &str) -> Self { - let pending = PendingDirectTurn::prepare( + let pending = PendingTurn::prepare( client_turn_id.to_string(), serde_json::from_value(serde_json::json!({ "type": "message", @@ -76,37 +76,37 @@ impl DirectTurnReservation { direct_tool_call_now_ms(), ) .expect("prepare pending turn"); - super::enqueue_direct_pending_turn(thread_id, pending).expect("enqueue test turn"); - let dispatched = claim_direct_pending_turn(thread_id).expect("claim test turn"); + super::enqueue_pending_turn(thread_id, pending).expect("enqueue test turn"); + let dispatched = claim_pending_turn(thread_id).expect("claim test turn"); Self::resume(thread_id, &dispatched) } } -impl Drop for DirectTurnReservation { +impl Drop for TurnReservation { fn drop(&mut self) { // 兜底:任务 panic、future 被丢弃、或今后在终态之前新增的 `?` 早退。 // 这类失败说不出原因,只给分类;能说清原因的错误必须由调用方在更早的地方显式收口。 let _ = self.finish_if_unfinished(DirectTurnTerminal::host_dropped()); // 占用的释放就是"这个项目腾出了跑回合的位置":踢一脚,队里排着的消息不必等下一次用户动作。 // panic 也走这里,所以队列不会因为一个任务炸掉而永久停住。 - kick_direct_queue_dispatch(Path::new(&self.thread_id)); + kick_queue_dispatch(Path::new(&self.thread_id)); } } /// 踢一脚:让队首那条待发消息有机会变成一轮真的回合。 /// /// 幂等,三个调用点:**入队之后**(队列空且没有回合在跑时,放行就是这一脚,不必再等一个调度周期)、 -/// **占用释放**(正常 / 失败 / panic 共用,见 [`DirectTurnReservation`] 的 `Drop`)、**中止路径** +/// **占用释放**(正常 / 失败 / panic 共用,见 [`TurnReservation`] 的 `Drop`)、**中止路径** /// (兜底释放时那一轮不会再有人替它收尾)。 /// /// 它什么都不返回:没有"放行失败"。踢不动就是现在不该跑——已经有回合在跑,或者队列是空的; /// 这两种情况都会在下一次释放时再踢。 -pub(crate) fn kick_direct_queue_dispatch(root: &Path) { - let thread_id = direct_thread_id_for_project(root); - let Some(dispatched) = claim_direct_pending_turn(&thread_id) else { +pub(crate) fn kick_queue_dispatch(root: &Path) { + let thread_id = thread_id_for_project(root); + let Some(dispatched) = claim_pending_turn(&thread_id) else { return; }; - let reservation = DirectTurnReservation::resume(&thread_id, &dispatched); + let reservation = TurnReservation::resume(&thread_id, &dispatched); let root = root.to_path_buf(); tauri::async_runtime::spawn(async move { run_dispatched_direct_turn(root, dispatched, reservation).await; @@ -119,8 +119,8 @@ pub(crate) fn kick_direct_queue_dispatch(root: &Path) { /// 命令的返回值上。 async fn run_dispatched_direct_turn( root: PathBuf, - dispatched: DirectDispatchedTurn, - reservation: DirectTurnReservation, + dispatched: DispatchedTurn, + reservation: TurnReservation, ) { let turn_id = dispatched.pending.client_turn_id.clone(); // 落盘即回合成立:放行之后必须留下这条用户消息,哪怕这一轮随后失败。 @@ -174,35 +174,32 @@ async fn run_dispatched_direct_turn( mod tests { use super::*; use crate::agent::{ - consume_direct_thread, direct_thread_turn_is_active, enqueue_direct_pending_turn, - subscribe_direct_thread, DirectThreadEvent, DirectTurnFailure, DirectTurnFailureKind, - PendingDirectTurn, + consume_thread, enqueue_pending_turn, subscribe_thread, thread_turn_is_active, + DirectTurnFailure, DirectTurnFailureKind, PendingTurn, ThreadEvent, }; use uuid::Uuid; /// 订阅并把 bootstrap 拿掉:之后的 `consume` 只返回这次订阅之后产生的事件。 fn watch(thread_id: &str) -> String { - let bootstrap = subscribe_direct_thread(thread_id); - let _ = consume_direct_thread(&bootstrap.subscription_id); + let bootstrap = subscribe_thread(thread_id); + let _ = consume_thread(&bootstrap.subscription_id); bootstrap.subscription_id } - fn pending(subscription_id: &str) -> Vec { - consume_direct_thread(subscription_id) - .expect("consume") - .events + fn pending(subscription_id: &str) -> Vec { + consume_thread(subscription_id).expect("consume").events } - fn turn_completed_events(events: &[DirectThreadEvent]) -> Vec<&DirectThreadEvent> { + fn turn_completed_events(events: &[ThreadEvent]) -> Vec<&ThreadEvent> { events .iter() - .filter(|event| matches!(event, DirectThreadEvent::TurnCompleted { .. })) + .filter(|event| matches!(event, ThreadEvent::TurnCompleted { .. })) .collect() } /// 一条待发消息。放行路径只读它的身份与产物,所以这里用最小可用形状。 - fn pending_turn(client_turn_id: &str) -> PendingDirectTurn { - PendingDirectTurn::prepare( + fn pending_turn(client_turn_id: &str) -> PendingTurn { + PendingTurn::prepare( client_turn_id.to_string(), serde_json::from_value(serde_json::json!({ "type": "message", @@ -226,7 +223,7 @@ mod tests { } fn enqueue(thread_id: &str, client_turn_id: &str) { - enqueue_direct_pending_turn(thread_id, pending_turn(client_turn_id)).expect("enqueue"); + enqueue_pending_turn(thread_id, pending_turn(client_turn_id)).expect("enqueue"); } /// 放行的占用只认**自己的 token**:认领写下的占用身份与这一轮绑死,别人的终态盖不上它。 @@ -234,20 +231,20 @@ mod tests { fn a_foreign_token_cannot_close_the_turn() { let thread = unique_thread("token"); enqueue(&thread, "turn-1"); - let dispatched = claim_direct_pending_turn(&thread).expect("claim head"); - let owner = DirectTurnReservation::resume(&thread, &dispatched); - let foreign = DirectTurnReservation::resume( + let dispatched = claim_pending_turn(&thread).expect("claim head"); + let owner = TurnReservation::resume(&thread, &dispatched); + let foreign = TurnReservation::resume( &thread, - &DirectDispatchedTurn { + &DispatchedTurn { token: "foreign-token".to_string(), pending: pending_turn("turn-1"), }, ); assert!(!foreign.finish_if_unfinished(DirectTurnTerminal::host_dropped())); - assert!(direct_thread_turn_is_active(&thread)); + assert!(thread_turn_is_active(&thread)); assert!(owner.finish_if_unfinished(DirectTurnTerminal::host_dropped())); - assert!(!direct_thread_turn_is_active(&thread)); + assert!(!thread_turn_is_active(&thread)); // 显式收口之后 Drop 不再补第二条:兜底只负责"没人写过"的那一种。 drop(foreign); drop(owner); @@ -259,11 +256,11 @@ mod tests { let thread = unique_thread("deep"); let subscription = watch(&thread); enqueue(&thread, "turn-1"); - let dispatched = claim_direct_pending_turn(&thread).expect("claim head"); - let reservation = DirectTurnReservation::resume(&thread, &dispatched); + let dispatched = claim_pending_turn(&thread).expect("claim head"); + let reservation = TurnReservation::resume(&thread, &dispatched); // 深层收口:真正跑完这一轮的代码算出来的终态。 - let deep = DirectThreadEvent::turn_completed_failed( + let deep = ThreadEvent::turn_completed_failed( DirectTurnFailure::new( DirectTurnFailureKind::Timeout, "等待模型回执超时".to_string(), @@ -271,7 +268,7 @@ mod tests { 2_000, ) .with_user_item_id(Some("direct-codex:turn-1:user")); - crate::agent::complete_direct_thread_turn(&thread, deep); + crate::agent::complete_turn(&thread, deep); assert!( !reservation.finish_if_unfinished(DirectTurnTerminal::host_dropped()), @@ -283,7 +280,7 @@ mod tests { let completed = turn_completed_events(&events); assert_eq!(completed.len(), 1, "一轮只许有一条终态:{events:?}"); match completed[0] { - DirectThreadEvent::TurnCompleted { failure, .. } => { + ThreadEvent::TurnCompleted { failure, .. } => { assert_eq!( failure.as_ref().map(|f| f.kind), Some(DirectTurnFailureKind::Timeout) @@ -300,8 +297,8 @@ mod tests { let subscription = watch(&thread); enqueue(&thread, "turn-1"); enqueue(&thread, "turn-2"); - let dispatched = claim_direct_pending_turn(&thread).expect("claim head"); - let reservation = DirectTurnReservation::resume(&thread, &dispatched); + let dispatched = claim_pending_turn(&thread).expect("claim head"); + let reservation = TurnReservation::resume(&thread, &dispatched); drop(reservation); @@ -309,12 +306,12 @@ mod tests { let events = pending(&subscription); let terminal = events .iter() - .position(|event| matches!(event, DirectThreadEvent::TurnCompleted { .. })) + .position(|event| matches!(event, ThreadEvent::TurnCompleted { .. })) .expect("第一条必须被收口"); assert!( matches!( events.get(terminal), - Some(DirectThreadEvent::TurnCompleted { status, failure: Some(failure), .. }) + Some(ThreadEvent::TurnCompleted { status, failure: Some(failure), .. }) if status == "failed" && failure.kind == DirectTurnFailureKind::HostDropped ), "{events:?}" @@ -322,7 +319,7 @@ mod tests { let next_started = events.iter().position(|event| { matches!( event, - DirectThreadEvent::TurnStarted { user_item_id, .. } + ThreadEvent::TurnStarted { user_item_id, .. } if user_item_id.as_deref() == Some("direct-codex:turn-2:user") ) }); @@ -336,19 +333,19 @@ mod tests { #[test] fn kicking_does_nothing_while_a_turn_is_open_or_the_queue_is_empty() { let empty = unique_thread("empty"); - kick_direct_queue_dispatch(Path::new(&empty)); - assert!(!direct_thread_turn_is_active(&empty)); + kick_queue_dispatch(Path::new(&empty)); + assert!(!thread_turn_is_active(&empty)); let busy = unique_thread("busy"); enqueue(&busy, "turn-1"); enqueue(&busy, "turn-2"); - let dispatched = claim_direct_pending_turn(&busy).expect("claim head"); - let reservation = DirectTurnReservation::resume(&busy, &dispatched); + let dispatched = claim_pending_turn(&busy).expect("claim head"); + let reservation = TurnReservation::resume(&busy, &dispatched); - kick_direct_queue_dispatch(Path::new(&busy)); + kick_queue_dispatch(Path::new(&busy)); // 仍在跑的那一轮没有被顶掉:占用身份还是第一条。 - assert!(direct_thread_turn_is_active(&busy)); + assert!(thread_turn_is_active(&busy)); drop(reservation); } @@ -359,7 +356,7 @@ mod tests { let thread = unique_thread("stale-cancel"); let subscription = watch(&thread); // 这一轮已经被放行(占用登记在 Thread Manager 上),但任务泄漏:拿不到可中断句柄。 - let leaked = DirectTurnReservation::accept_for_test(&thread, "turn-stale-1"); + let leaked = TurnReservation::accept_for_test(&thread, "turn-stale-1"); enqueue(&thread, "turn-stale-2"); // "从没进执行器"只对过了启动窗口的残留放行;真实用例等不了 60 秒。 @@ -369,7 +366,7 @@ mod tests { .expect("stale release"); assert_eq!( view.outcome, - super::super::codex_app_server::DIRECT_TURN_CANCEL_OUTCOME_RELEASED + crate::agent::codex_app_server::DIRECT_TURN_CANCEL_OUTCOME_RELEASED ); assert_eq!(view.client_turn_id, "turn-stale-1"); @@ -378,14 +375,14 @@ mod tests { assert!( events.iter().any(|event| matches!( event, - DirectThreadEvent::TurnCompleted { status, .. } if status == "aborted" + ThreadEvent::TurnCompleted { status, .. } if status == "aborted" )), "{events:?}" ); assert!( events.iter().any(|event| matches!( event, - DirectThreadEvent::TurnStarted { user_item_id, .. } + ThreadEvent::TurnStarted { user_item_id, .. } if user_item_id.as_deref() == Some("direct-codex:turn-stale-2:user") )), "队首必须在兜底释放之后被放行:{events:?}" diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_manager.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/mod.rs similarity index 80% rename from apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_manager.rs rename to apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/mod.rs index 826577703..00786e8fe 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_manager.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/mod.rs @@ -4,30 +4,37 @@ //! 每个 subscriber 一个受保护的消费游标。它不理解前端 reducer,也不负责 JSONL //! 持久化;调用方必须在完成 item 持久化成功后再追加对应完成事件。 +pub(crate) mod dispatch; +pub(crate) mod queue; +pub(crate) mod wire; + +pub(crate) use dispatch::*; +pub(crate) use queue::*; +pub(crate) use wire::*; + use std::collections::{HashMap, HashSet}; use std::sync::{Mutex, OnceLock}; use uuid::Uuid; use crate::agent::{ direct_codex_user_item_id_for_client_turn_id, direct_tool_call_now_ms, queue_has_room, - DirectQueueRemovalOutcome, DirectQueueRemovalReason, DirectThreadConsumeResult, - DirectThreadEvent, DirectThreadSubscriptionBootstrap, EnqueueOutcome, EnqueueRejection, - PendingDirectTurn, + ConsumeResult, EnqueueOutcome, EnqueueRejection, PendingTurn, QueueRemovalOutcome, + QueueRemovalReason, SubscriptionBootstrap, ThreadEvent, }; const DEFAULT_MAX_EVENTS: usize = 8_192; const DEFAULT_MAX_BYTES: usize = 8 * 1024 * 1024; pub(crate) const SUBSCRIPTION_EXPIRED: &str = "SUBSCRIPTION_EXPIRED"; -pub(crate) const DIRECT_THREAD_NOTIFY_EVENT: &str = "game-creator-direct-thread-notify"; +pub(crate) const THREAD_NOTIFY_EVENT: &str = "game-creator-direct-thread-notify"; -static DIRECT_THREAD_MANAGER: OnceLock> = OnceLock::new(); -static DIRECT_THREAD_MANAGER_APP_HANDLE: OnceLock = OnceLock::new(); +static THREAD_MANAGER: OnceLock> = OnceLock::new(); +static THREAD_MANAGER_APP_HANDLE: OnceLock = OnceLock::new(); /// 队列里的一个事件。`seq` 只服务内部游标,不下发:前端按 `consume` 返回的数组顺序处理。 #[derive(Clone, Debug)] struct StoredEvent { - event: DirectThreadEvent, + event: ThreadEvent, seq: u64, bytes: usize, cleanable: bool, @@ -35,8 +42,8 @@ struct StoredEvent { /// /// 只有 `queue.enqueued` 会带它,而且**它本身就是队列成员身份**:`Some` = 这条待发消息还在队, /// 被取走 = 已经离开队列。队列的先后就是事件的先后,所以没有第二张队列表要对齐,也不会出现 - /// "事件说在队、队列表说不在"的中间态(见 `direct_thread_queue`)。 - pending: Option, + /// "事件说在队、队列表说不在"的中间态(见 `thread_manager::queue`)。 + pending: Option, } #[derive(Clone, Debug)] @@ -46,8 +53,8 @@ struct SubscriberState { /// 一条正在跑的逻辑回合的占用:放行时登记,终态写出时解除。 /// -/// 它同时是首页「运行中的项目」快照的**唯一事实源**([`list_direct_active_turns`]):这一格的 -/// 生命周期就是"这一轮在不在跑",进度字段由运行时那一侧经 [`update_direct_thread_active_turn`] +/// 它同时是首页「运行中的项目」快照的**唯一事实源**([`list_active_turns`]):这一格的 +/// 生命周期就是"这一轮在不在跑",进度字段由运行时那一侧经 [`update_active_turn`] /// 回填。任务侧不再另建一张活动回合表——同一件事只许有一处真相。 /// /// 两个身份别混: @@ -55,7 +62,7 @@ struct SubscriberState { /// 避免迟到的旧占用把新回合的边界顶掉。它不对外。 /// - `turn_id` 是给界面看的回合身份(`clientTurnId` 派生),只服务快照与进度回填的匹配。 #[derive(Clone, Debug)] -struct ActiveDirectTurn { +struct ActiveTurn { token: String, turn_id: String, project_name: Option, @@ -69,11 +76,11 @@ struct ActiveDirectTurn { /// 首页「运行中的项目」的一条快照。 /// /// `project_path` 与线上其它地方的项目身份取同一个字符串:Thread Manager 的线程身份就是项目的 -/// canonical 路径(见 `direct_thread_id_for_project`),所以快照里的项目身份与事件流里的身份 +/// canonical 路径(见 `thread_id_for_project`),所以快照里的项目身份与事件流里的身份 /// 永远能对上,不需要调用方再做一次归一。 #[derive(Clone, Debug, serde::Serialize)] #[serde(rename_all = "camelCase")] -pub(crate) struct DirectActiveTurnSnapshot { +pub(crate) struct ActiveTurnSnapshot { pub(crate) project_path: String, pub(crate) project_name: Option, pub(crate) turn_id: String, @@ -88,9 +95,9 @@ pub(crate) struct DirectActiveTurnSnapshot { /// 工具桥、校验预约、上下文预取)。 /// /// 身份取自 Thread Manager 的活动回合登记——"这一轮在不在跑"只有这一处真相,界面快照 -/// ([`list_direct_active_turns`])与放行闸门读的也是它,不再另立第二份记录。 +/// ([`list_active_turns`])与放行闸门读的也是它,不再另立第二份记录。 #[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) struct DirectTurnIdentity { +pub(crate) struct TurnIdentity { /// 与事件 `turnId` 同一个身份(`clientTurnId`)。 pub(crate) client_turn_id: String, /// 这一轮被放行(登记占用)的时刻(Unix 毫秒)。 @@ -100,9 +107,9 @@ pub(crate) struct DirectTurnIdentity { /// 兜底释放一个残留回合的结果。 /// /// 除了 [`Self::Released`],其余三种都是**什么都没写**:占用没动、事件没发。调用方据此给用户 -/// 不同的话;把"校验"和"释放"拆成两步就会丢掉这个性质(见 [`DirectThreadManager::release_stale_turn`])。 +/// 不同的话;把"校验"和"释放"拆成两步就会丢掉这个性质(见 [`ThreadManager::release_stale_turn`])。 #[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) enum DirectStaleTurnRelease { +pub(crate) enum StaleTurnRelease { /// 占用已解除,`turn.completed{aborted}` 已下发:这个 thread 现在可以放行下一条。 Released(String), /// 这个 thread 上没有未收口的回合。 @@ -118,9 +125,9 @@ pub(crate) enum DirectStaleTurnRelease { /// `token` 是这一轮的占用身份(终态出口只认它),`pending` 是放行要用的**全部**输入——放行不再 /// 重跑任何检查,也就不需要再回到调用方取参数。 #[derive(Clone, Debug)] -pub(crate) struct DirectDispatchedTurn { +pub(crate) struct DispatchedTurn { pub(crate) token: String, - pub(crate) pending: PendingDirectTurn, + pub(crate) pending: PendingTurn, } #[derive(Clone, Debug)] @@ -132,7 +139,7 @@ struct ThreadState { active_items: HashSet, unresolved_requests: HashSet, /// 未收口的逻辑回合。`None` 表示这个 thread 没有正在跑的回合。 - active_turn: Option, + active_turn: Option, /// 最近一条 `turn.started` / `turn.completed` 的独立拷贝。 /// /// TODO(thread-manager): 这里有意只保留"锚点",因为 replay 队列会回收可回收事件, @@ -140,7 +147,7 @@ struct ThreadState { /// 只有这份拷贝保证新订阅仍能判定"最新回合是否还在跑"。若将来需要回放多个回合的 /// 生命周期(回合账本、跨进程恢复、按回合统计),必须另建持久 ledger, /// 不能靠扩大这份拷贝或放宽回收规则来模拟。 - lifecycle_anchor: Option<(u64, DirectThreadEvent)>, + lifecycle_anchor: Option<(u64, ThreadEvent)>, subscribers: HashMap, } @@ -161,19 +168,19 @@ impl Default for ThreadState { } #[derive(Clone, Debug)] -pub(crate) struct DirectThreadManager { +pub(crate) struct ThreadManager { threads: HashMap, max_events: usize, max_bytes: usize, } -impl Default for DirectThreadManager { +impl Default for ThreadManager { fn default() -> Self { Self::new() } } -impl DirectThreadManager { +impl ThreadManager { pub(crate) fn new() -> Self { Self::with_limits(DEFAULT_MAX_EVENTS, DEFAULT_MAX_BYTES) } @@ -186,11 +193,7 @@ impl DirectThreadManager { } } - pub(crate) fn append( - &mut self, - thread_id: &str, - event: DirectThreadEvent, - ) -> DirectThreadEvent { + pub(crate) fn append(&mut self, thread_id: &str, event: ThreadEvent) -> ThreadEvent { self.append_inner(thread_id, event, None) } @@ -198,9 +201,9 @@ impl DirectThreadManager { fn append_inner( &mut self, thread_id: &str, - event: DirectThreadEvent, - pending: Option, - ) -> DirectThreadEvent { + event: ThreadEvent, + pending: Option, + ) -> ThreadEvent { let thread = self.threads.entry(thread_id.to_string()).or_default(); thread.next_seq = thread.next_seq.saturating_add(1); let seq = thread.next_seq; @@ -228,7 +231,7 @@ impl DirectThreadManager { event } - pub(crate) fn subscribe(&mut self, thread_id: &str) -> DirectThreadSubscriptionBootstrap { + pub(crate) fn subscribe(&mut self, thread_id: &str) -> SubscriptionBootstrap { let thread = self.threads.entry(thread_id.to_string()).or_default(); let subscription_id = Uuid::new_v4().to_string(); let cursor = thread.next_seq; @@ -250,7 +253,7 @@ impl DirectThreadManager { } } events.sort_by_key(|(seq, _)| *seq); - DirectThreadSubscriptionBootstrap { + SubscriptionBootstrap { subscription_id, last_completed_item_id: None, events: events.into_iter().map(|(_, event)| event).collect(), @@ -290,12 +293,12 @@ impl DirectThreadManager { } /// 首页快照:只导出仍有未收口逻辑回合的 thread。 - fn active_turn_snapshots(&self) -> Vec { + fn active_turn_snapshots(&self) -> Vec { self.threads .iter() .filter_map(|(thread_id, thread)| { let active = thread.active_turn.as_ref()?; - Some(DirectActiveTurnSnapshot { + Some(ActiveTurnSnapshot { project_path: thread_id.clone(), project_name: active.project_name.clone(), turn_id: active.turn_id.clone(), @@ -313,7 +316,7 @@ impl DirectThreadManager { /// /// 不校验 token:这一条由真正跑完这一轮的代码调用,终态就是它算出来的那个(CLI 这类没有 /// 占用登记的入口也走这里,保持"终态一定下发"的既有语义)。 - fn complete_turn(&mut self, thread_id: &str, event: DirectThreadEvent) -> DirectThreadEvent { + fn complete_turn(&mut self, thread_id: &str, event: ThreadEvent) -> ThreadEvent { if let Some(thread) = self.threads.get_mut(thread_id) { thread.active_turn = None; } @@ -327,7 +330,7 @@ impl DirectThreadManager { &mut self, thread_id: &str, token: &str, - event: DirectThreadEvent, + event: ThreadEvent, ) -> bool { let reserved = match self.threads.get_mut(thread_id) { Some(thread) => match thread.active_turn.as_ref() { @@ -352,7 +355,7 @@ impl DirectThreadManager { /// 队列顺势认领了下一条并登记新占用——那时再无条件解除占用,就会清掉**新那一轮**的占用,还给 /// 已经结束的老回合补一条假终态。这正是"多轮同时在跑 / 终态张冠李戴"那一类事故。 /// - /// 三条前置条件与 [`DirectStaleTurnRelease`] 的三个失败分支一一对应: + /// 三条前置条件与 [`StaleTurnRelease`] 的三个失败分支一一对应: /// 1. 这个 thread 上确实登记着一轮; /// 2. 传了 `expected_client_turn_id` 时必须与登记一致; /// 3. `min_age_ms` 有值时,登记的年龄(`now_ms - started_at`)必须不小于它——用来排除 @@ -366,48 +369,45 @@ impl DirectThreadManager { expected_client_turn_id: Option<&str>, min_age_ms: Option, now_ms: u64, - ) -> DirectStaleTurnRelease { + ) -> StaleTurnRelease { let released = { let Some(thread) = self.threads.get_mut(thread_id) else { - return DirectStaleTurnRelease::NoActiveTurn; + return StaleTurnRelease::NoActiveTurn; }; let Some(active) = thread.active_turn.take() else { - return DirectStaleTurnRelease::NoActiveTurn; + return StaleTurnRelease::NoActiveTurn; }; if expected_client_turn_id.is_some_and(|expected| expected != active.turn_id.as_str()) { // 身份对不上:放回去,绝不误伤另一条回合。 thread.active_turn = Some(active); - return DirectStaleTurnRelease::AnotherTurnRunning; + return StaleTurnRelease::AnotherTurnRunning; } let age_ms = now_ms.saturating_sub(active.started_at); if min_age_ms.is_some_and(|min_age_ms| age_ms < min_age_ms) { thread.active_turn = Some(active); - return DirectStaleTurnRelease::WithinStartupWindow { age_ms }; + return StaleTurnRelease::WithinStartupWindow { age_ms }; } active.turn_id }; // 这一轮不会再有人替它发终态(执行进程已退出 / 从没进执行器):补一条 aborted,否则前端的 // "最新回合是否在跑"会永远停在运行中。身份用被释放那一轮自己的 `turn_id` 推导,与放行时 - // `turn.started` 的口径同一份(`PendingDirectTurn::user_item_id`)。 + // `turn.started` 的口径同一份(`PendingTurn::user_item_id`)。 self.append( thread_id, - DirectThreadEvent::turn_completed("aborted".to_string(), now_ms).with_user_item_id( + ThreadEvent::turn_completed("aborted".to_string(), now_ms).with_user_item_id( direct_codex_user_item_id_for_client_turn_id(&released).as_deref(), ), ); - DirectStaleTurnRelease::Released(released) + StaleTurnRelease::Released(released) } /// 这个 thread 上正在跑的那一轮身份;`None` = 没有未收口的回合。 - fn active_turn_identity(&self, thread_id: &str) -> Option { + fn active_turn_identity(&self, thread_id: &str) -> Option { self.threads.get(thread_id).and_then(|thread| { - thread - .active_turn - .as_ref() - .map(|active| DirectTurnIdentity { - client_turn_id: active.turn_id.clone(), - started_at: active.started_at, - }) + thread.active_turn.as_ref().map(|active| TurnIdentity { + client_turn_id: active.turn_id.clone(), + started_at: active.started_at, + }) }) } @@ -418,7 +418,7 @@ impl DirectThreadManager { } /// 在队的待发消息,队首在前。 - fn pending_turns(thread: &ThreadState) -> Vec<&PendingDirectTurn> { + fn pending_turns(thread: &ThreadState) -> Vec<&PendingTurn> { thread .events .iter() @@ -434,7 +434,7 @@ impl DirectThreadManager { fn enqueue_pending_turn( &mut self, thread_id: &str, - turn: PendingDirectTurn, + turn: PendingTurn, ) -> Result { { let thread = self.threads.entry(thread_id.to_string()).or_default(); @@ -463,17 +463,17 @@ impl DirectThreadManager { thread_id: &str, client_turn_id: &str, at_ms: u64, - ) -> DirectQueueRemovalOutcome { + ) -> QueueRemovalOutcome { let removed = { let Some(thread) = self.threads.get_mut(thread_id) else { - return DirectQueueRemovalOutcome::NotFound; + return QueueRemovalOutcome::NotFound; }; if thread .active_turn .as_ref() .is_some_and(|active| active.turn_id == client_turn_id) { - return DirectQueueRemovalOutcome::AlreadyDispatched; + return QueueRemovalOutcome::AlreadyDispatched; } let head = thread.head; let mut removed = false; @@ -491,17 +491,17 @@ impl DirectThreadManager { removed }; if !removed { - return DirectQueueRemovalOutcome::NotFound; + return QueueRemovalOutcome::NotFound; } self.append( thread_id, - DirectThreadEvent::queue_removed( + ThreadEvent::queue_removed( client_turn_id.to_string(), - DirectQueueRemovalReason::Cancelled, + QueueRemovalReason::Cancelled, at_ms, ), ); - DirectQueueRemovalOutcome::Removed + QueueRemovalOutcome::Removed } /// 放行:同一个临界区里取队首 → 登记占用 → 写 `turn.started` 与 `queue.removed{dispatched}`。 @@ -513,7 +513,7 @@ impl DirectThreadManager { &mut self, thread_id: &str, started_at_ms: u64, - ) -> Option { + ) -> Option { let pending = { let thread = self.threads.get_mut(thread_id)?; if thread.active_turn.is_some() { @@ -534,7 +534,7 @@ impl DirectThreadManager { .and_then(|name| name.to_str()) .map(str::to_string); if let Some(thread) = self.threads.get_mut(thread_id) { - thread.active_turn = Some(ActiveDirectTurn { + thread.active_turn = Some(ActiveTurn { token: token.clone(), turn_id: turn_id.clone(), project_name, @@ -549,18 +549,13 @@ impl DirectThreadManager { let user_item_id = pending.user_item_id(); self.append( thread_id, - DirectThreadEvent::turn_started(started_at_ms) - .with_user_item_id(user_item_id.as_deref()), + ThreadEvent::turn_started(started_at_ms).with_user_item_id(user_item_id.as_deref()), ); self.append( thread_id, - DirectThreadEvent::queue_removed( - turn_id, - DirectQueueRemovalReason::Dispatched, - started_at_ms, - ), + ThreadEvent::queue_removed(turn_id, QueueRemovalReason::Dispatched, started_at_ms), ); - Some(DirectDispatchedTurn { token, pending }) + Some(DispatchedTurn { token, pending }) } fn subscriber_ids(&self, thread_id: &str) -> Vec { @@ -570,10 +565,7 @@ impl DirectThreadManager { .unwrap_or_default() } - pub(crate) fn consume( - &mut self, - subscription_id: &str, - ) -> Result { + pub(crate) fn consume(&mut self, subscription_id: &str) -> Result { let Some((_, thread)) = self .threads .iter_mut() @@ -611,7 +603,7 @@ impl DirectThreadManager { .get_mut(subscription_id) .expect("subscriber remains registered") .cursor = last_seq; - let result = DirectThreadConsumeResult { events }; + let result = ConsumeResult { events }; Self::trim_prefix(thread); Ok(result) } @@ -667,23 +659,23 @@ impl DirectThreadManager { } /// 观察一条事件,返回它本身是否可回收。 - fn observe_event(thread: &mut ThreadState, seq: u64, event: &DirectThreadEvent) -> bool { + fn observe_event(thread: &mut ThreadState, seq: u64, event: &ThreadEvent) -> bool { match event { - DirectThreadEvent::ItemStarted { item, .. } => { + ThreadEvent::ItemStarted { item, .. } => { thread.active_items.insert(item.item_id().to_string()); false } - DirectThreadEvent::ItemCompleted { item, .. } => { + ThreadEvent::ItemCompleted { item, .. } => { thread.active_items.remove(item.item_id()); true } - DirectThreadEvent::TurnStarted { .. } | DirectThreadEvent::TurnCompleted { .. } => { + ThreadEvent::TurnStarted { .. } | ThreadEvent::TurnCompleted { .. } => { // 队列只保留最新一条生命周期事件,更早的可能已被回收;见 // `ThreadState::lifecycle_anchor` 的 TODO:这不是完整事件日志。 thread.lifecycle_anchor = Some((seq, event.clone())); true } - DirectThreadEvent::Request { + ThreadEvent::Request { kind, request_id, .. } => { if kind.is_request() { @@ -698,12 +690,12 @@ impl DirectThreadManager { } true } - DirectThreadEvent::ItemDelta { .. } => true, + ThreadEvent::ItemDelta { .. } => true, // 待发消息在队期间不可回收:队列的成员与顺序**就是**这条事件本身 - // (见 `direct_thread_queue`)。bootstrap 因此天然看得见当前队列。 - DirectThreadEvent::QueueEnqueued { .. } => false, + // (见 `thread_manager::queue`)。bootstrap 因此天然看得见当前队列。 + ThreadEvent::QueueEnqueued { .. } => false, // 离开队列(取消 / 放行)才把同一条待发消息的入队事件一起转成可回收。 - DirectThreadEvent::QueueRemoved { client_turn_id, .. } => { + ThreadEvent::QueueRemoved { client_turn_id, .. } => { Self::mark_queue_events_cleanable(thread, client_turn_id); true } @@ -832,41 +824,38 @@ impl DirectThreadManager { } } -fn global_direct_thread_manager() -> &'static Mutex { - DIRECT_THREAD_MANAGER.get_or_init(|| Mutex::new(DirectThreadManager::new())) +fn global_thread_manager() -> &'static Mutex { + THREAD_MANAGER.get_or_init(|| Mutex::new(ThreadManager::new())) } -pub(crate) fn set_direct_thread_manager_app_handle(app: tauri::AppHandle) { - let _ = DIRECT_THREAD_MANAGER_APP_HANDLE.set(app); +pub(crate) fn set_thread_manager_app_handle(app: tauri::AppHandle) { + let _ = THREAD_MANAGER_APP_HANDLE.set(app); } -pub(crate) fn append_direct_thread_event( - thread_id: &str, - event: DirectThreadEvent, -) -> DirectThreadEvent { +pub(crate) fn append_thread_event(thread_id: &str, event: ThreadEvent) -> ThreadEvent { let event = { - global_direct_thread_manager() + global_thread_manager() .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) .append(thread_id, event) }; - notify_direct_thread_subscribers(thread_id); + notify_subscribers(thread_id); event } -/// 入队一条待发消息(队列的成员与顺序就是事件列表,见 `direct_thread_queue`)。 -pub(crate) fn enqueue_direct_pending_turn( +/// 入队一条待发消息(队列的成员与顺序就是事件列表,见 `thread_manager::queue`)。 +pub(crate) fn enqueue_pending_turn( thread_id: &str, - turn: PendingDirectTurn, + turn: PendingTurn, ) -> Result { let outcome = { - global_direct_thread_manager() + global_thread_manager() .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) .enqueue_pending_turn(thread_id, turn) }; if matches!(outcome, Ok(EnqueueOutcome::Enqueued)) { - notify_direct_thread_subscribers(thread_id); + notify_subscribers(thread_id); } outcome } @@ -875,52 +864,49 @@ pub(crate) fn enqueue_direct_pending_turn( /// /// 给入队侧做最便宜的一次容量预判用:满了就先拒绝,不去做工程准备那种分钟级、会在磁盘上留下产物的 /// 活。权威判据仍然是入队临界区里的 `queue_has_room`,这里只求早失败。 -pub(crate) fn direct_pending_turn_count(thread_id: &str) -> usize { - global_direct_thread_manager() +pub(crate) fn pending_turn_count(thread_id: &str) -> usize { + global_thread_manager() .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) .pending_turn_count(thread_id) } /// 取消一条待发消息:结果告诉调用方它是被取消、已经放行,还是本来就不在队里。 -pub(crate) fn remove_direct_pending_turn( - thread_id: &str, - client_turn_id: &str, -) -> DirectQueueRemovalOutcome { +pub(crate) fn remove_pending_turn(thread_id: &str, client_turn_id: &str) -> QueueRemovalOutcome { let outcome = { - global_direct_thread_manager() + global_thread_manager() .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) .remove_pending_turn(thread_id, client_turn_id, direct_tool_call_now_ms()) }; - if outcome == DirectQueueRemovalOutcome::Removed { - notify_direct_thread_subscribers(thread_id); + if outcome == QueueRemovalOutcome::Removed { + notify_subscribers(thread_id); } outcome } -/// 认领队首并登记占用(放行的原子步骤,见 [`DirectThreadManager::claim_pending_turn`])。 +/// 认领队首并登记占用(放行的原子步骤,见 [`ThreadManager::claim_pending_turn`])。 /// /// `None` = 现在不该放行:已有未收口的回合,或队列为空。 /// /// 认领成功就是首页「运行中的项目」快照变了的那一刻:放行这一步同时登记占用,所以在这里广播 /// [`crate::agent::emit_direct_active_turns_changed`]。入队与取消只改队列,不进快照,不广播。 -pub(crate) fn claim_direct_pending_turn(thread_id: &str) -> Option { +pub(crate) fn claim_pending_turn(thread_id: &str) -> Option { let claimed = { - global_direct_thread_manager() + global_thread_manager() .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) .claim_pending_turn(thread_id, direct_tool_call_now_ms()) }; if claimed.is_some() { - notify_direct_thread_subscribers(thread_id); + notify_subscribers(thread_id); crate::agent::emit_direct_active_turns_changed(); } claimed } /// 运行时回填某一轮逻辑回合的进度(状态 / 活动 / 序号)。返回快照内容是否真的变了。 -pub(crate) fn update_direct_thread_active_turn( +pub(crate) fn update_active_turn( thread_id: &str, turn_id: &str, status: &str, @@ -928,7 +914,7 @@ pub(crate) fn update_direct_thread_active_turn( sequence: u64, updated_at: u64, ) -> bool { - let changed = global_direct_thread_manager() + let changed = global_thread_manager() .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) .update_active_turn(thread_id, turn_id, status, activity, sequence, updated_at); @@ -938,9 +924,9 @@ pub(crate) fn update_direct_thread_active_turn( changed } -/// 首页「运行中的项目」快照:逻辑回合的唯一导出口(见 [`DirectActiveTurnSnapshot`])。 -pub(crate) fn list_direct_active_turns() -> Result, String> { - let mut turns = global_direct_thread_manager() +/// 首页「运行中的项目」快照:逻辑回合的唯一导出口(见 [`ActiveTurnSnapshot`])。 +pub(crate) fn list_active_turns() -> Result, String> { + let mut turns = global_thread_manager() .lock() .map_err(|_| "Direct 线程管理器已损坏".to_string())? .active_turn_snapshots(); @@ -949,46 +935,42 @@ pub(crate) fn list_direct_active_turns() -> Result } /// 深层终态出口:解除占用并写 `turn.completed`。 -pub(crate) fn complete_direct_thread_turn(thread_id: &str, event: DirectThreadEvent) { +pub(crate) fn complete_turn(thread_id: &str, event: ThreadEvent) { { - global_direct_thread_manager() + global_thread_manager() .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) .complete_turn(thread_id, event); } - notify_direct_thread_subscribers(thread_id); + notify_subscribers(thread_id); crate::agent::emit_direct_active_turns_changed(); } /// 占用对象的兜底出口:仍被同一 token 占用时才写,返回是否写了。 -pub(crate) fn complete_direct_thread_turn_if_reserved( - thread_id: &str, - token: &str, - event: DirectThreadEvent, -) -> bool { +pub(crate) fn complete_turn_if_reserved(thread_id: &str, token: &str, event: ThreadEvent) -> bool { let written = { - global_direct_thread_manager() + global_thread_manager() .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) .complete_turn_if_reserved(thread_id, token, event) }; if written { - notify_direct_thread_subscribers(thread_id); + notify_subscribers(thread_id); crate::agent::emit_direct_active_turns_changed(); } written } -/// 兜底释放一个残留回合:校验与释放**同一个临界区**(见 [`DirectThreadManager::release_stale_turn`])。 +/// 兜底释放一个残留回合:校验与释放**同一个临界区**(见 [`ThreadManager::release_stale_turn`])。 /// /// 只有真释放了才通知订阅者:校验不过时什么都没写,不该惊动任何人。 pub(crate) fn release_stale_direct_turn( thread_id: &str, expected_client_turn_id: Option<&str>, min_age_ms: Option, -) -> DirectStaleTurnRelease { +) -> StaleTurnRelease { let outcome = { - global_direct_thread_manager() + global_thread_manager() .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) .release_stale_turn( @@ -998,8 +980,8 @@ pub(crate) fn release_stale_direct_turn( direct_tool_call_now_ms(), ) }; - if matches!(outcome, DirectStaleTurnRelease::Released(_)) { - notify_direct_thread_subscribers(thread_id); + if matches!(outcome, StaleTurnRelease::Released(_)) { + notify_subscribers(thread_id); } outcome } @@ -1007,16 +989,16 @@ pub(crate) fn release_stale_direct_turn( /// 这个 thread 上正在跑的那一轮身份。 /// /// 只读:占用仍然只由终态出口(深层终态 / 占用对象兜底 / 兜底释放)解除,这里不改任何状态。 -pub(crate) fn read_direct_turn_identity(thread_id: &str) -> Option { - global_direct_thread_manager() +pub(crate) fn read_turn_identity(thread_id: &str) -> Option { + global_thread_manager() .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) .active_turn_identity(thread_id) } /// 这个 thread 是否还有没收口的逻辑回合。 -pub(crate) fn direct_thread_turn_is_active(thread_id: &str) -> bool { - global_direct_thread_manager() +pub(crate) fn thread_turn_is_active(thread_id: &str) -> bool { + global_thread_manager() .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) .turn_is_active(thread_id) @@ -1028,7 +1010,7 @@ pub(crate) fn direct_thread_turn_is_active(thread_id: &str) -> bool { /// 真实用例等不了 60 秒。 #[cfg(test)] pub(crate) fn backdate_direct_active_turn_for_test(thread_id: &str, age_ms: u64) { - let mut manager = global_direct_thread_manager() + let mut manager = global_thread_manager() .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()); if let Some(active) = manager @@ -1040,42 +1022,40 @@ pub(crate) fn backdate_direct_active_turn_for_test(thread_id: &str, age_ms: u64) } } -fn notify_direct_thread_subscribers(thread_id: &str) { +fn notify_subscribers(thread_id: &str) { let subscriber_ids = { - let manager = global_direct_thread_manager() + let manager = global_thread_manager() .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()); manager.subscriber_ids(thread_id) }; - if let Some(app) = DIRECT_THREAD_MANAGER_APP_HANDLE.get() { + if let Some(app) = THREAD_MANAGER_APP_HANDLE.get() { for subscription_id in subscriber_ids { let _ = tauri::Emitter::emit( app, - DIRECT_THREAD_NOTIFY_EVENT, + THREAD_NOTIFY_EVENT, serde_json::json!({ "subscriptionId": subscription_id }), ); } } } -pub(crate) fn subscribe_direct_thread(thread_id: &str) -> DirectThreadSubscriptionBootstrap { - global_direct_thread_manager() +pub(crate) fn subscribe_thread(thread_id: &str) -> SubscriptionBootstrap { + global_thread_manager() .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) .subscribe(thread_id) } -pub(crate) fn consume_direct_thread( - subscription_id: &str, -) -> Result { - global_direct_thread_manager() +pub(crate) fn consume_thread(subscription_id: &str) -> Result { + global_thread_manager() .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) .consume(subscription_id) } -pub(crate) fn unsubscribe_direct_thread(subscription_id: &str) { - let mut manager = global_direct_thread_manager() +pub(crate) fn unsubscribe_thread(subscription_id: &str) { + let mut manager = global_thread_manager() .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()); manager.unsubscribe(subscription_id); @@ -1084,12 +1064,10 @@ pub(crate) fn unsubscribe_direct_thread(subscription_id: &str) { #[cfg(test)] mod tests { use super::*; - use crate::agent::{ - DirectThreadDeltaKind, DirectThreadItem, DirectThreadRequestKind, MAX_PENDING_DIRECT_TURNS, - }; + use crate::agent::{ThreadDeltaKind, ThreadItem, ThreadRequestKind, MAX_PENDING_TURNS}; - fn message(item_id: &str) -> DirectThreadItem { - DirectThreadItem::Message { + fn message(item_id: &str) -> ThreadItem { + ThreadItem::Message { item_id: item_id.to_string(), role: "assistant".to_string(), text: "内容".to_string(), @@ -1101,30 +1079,30 @@ mod tests { /// 它们断言的是队列 / 游标语义,不是时间本身。 const FIXED_AT_MS: u64 = 1_000; - fn item_started(item_id: &str) -> DirectThreadEvent { - DirectThreadEvent::item_started(message(item_id), FIXED_AT_MS) + fn item_started(item_id: &str) -> ThreadEvent { + ThreadEvent::item_started(message(item_id), FIXED_AT_MS) } - fn item_completed(item_id: &str) -> DirectThreadEvent { - DirectThreadEvent::item_completed(message(item_id), FIXED_AT_MS) + fn item_completed(item_id: &str) -> ThreadEvent { + ThreadEvent::item_completed(message(item_id), FIXED_AT_MS) } - fn item_delta(item_id: &str) -> DirectThreadEvent { - DirectThreadEvent::item_delta( + fn item_delta(item_id: &str) -> ThreadEvent { + ThreadEvent::item_delta( item_id.to_string(), - DirectThreadDeltaKind::Message, + ThreadDeltaKind::Message, "增量".to_string(), ) } - fn request(kind: DirectThreadRequestKind, request_id: Option<&str>) -> DirectThreadEvent { - DirectThreadEvent::request(kind, request_id.map(str::to_string)) + fn request(kind: ThreadRequestKind, request_id: Option<&str>) -> ThreadEvent { + ThreadEvent::request(kind, request_id.map(str::to_string)) } #[test] fn subscribers_have_independent_cursors_on_one_global_queue() { - let mut manager = DirectThreadManager::with_limits(100, 100_000); - manager.append("thread-1", DirectThreadEvent::turn_started(FIXED_AT_MS)); + let mut manager = ThreadManager::with_limits(100, 100_000); + manager.append("thread-1", ThreadEvent::turn_started(FIXED_AT_MS)); let first = manager.subscribe("thread-1"); let second = manager.subscribe("thread-1"); manager.append("thread-1", item_started("item-1")); @@ -1147,7 +1125,7 @@ mod tests { #[test] fn unsubscribe_releases_the_subscriber_and_is_idempotent() { - let mut manager = DirectThreadManager::with_limits(100, 100_000); + let mut manager = ThreadManager::with_limits(100, 100_000); manager.append("thread-1", item_started("item-1")); let subscription = manager.subscribe("thread-1"); manager.append("thread-1", item_delta("item-1")); @@ -1162,7 +1140,7 @@ mod tests { #[test] fn unsubscribe_allows_a_cleanable_prefix_to_be_trimmed() { - let mut manager = DirectThreadManager::with_limits(100, 100_000); + let mut manager = ThreadManager::with_limits(100, 100_000); manager.append("thread-1", item_started("item-1")); let subscription = manager.subscribe("thread-1"); manager.append("thread-1", item_completed("item-1")); @@ -1175,8 +1153,8 @@ mod tests { #[test] fn bootstrap_contains_lifecycle_anchor_and_unfinished_events_only() { - let mut manager = DirectThreadManager::with_limits(100, 100_000); - manager.append("thread-1", DirectThreadEvent::turn_started(FIXED_AT_MS)); + let mut manager = ThreadManager::with_limits(100, 100_000); + manager.append("thread-1", ThreadEvent::turn_started(FIXED_AT_MS)); manager.append("thread-1", item_started("item-1")); manager.append("thread-1", item_delta("item-1")); manager.append("thread-1", item_completed("item-1")); @@ -1186,15 +1164,15 @@ mod tests { assert!(matches!( bootstrap.events.as_slice(), [ - DirectThreadEvent::TurnStarted { .. }, - DirectThreadEvent::ItemStarted { item, .. }, + ThreadEvent::TurnStarted { .. }, + ThreadEvent::ItemStarted { item, .. }, ] if item.item_id() == "item-2" )); } #[test] fn completion_releases_item_events_only_after_the_completion_event_is_appended() { - let mut manager = DirectThreadManager::with_limits(100, 100_000); + let mut manager = ThreadManager::with_limits(100, 100_000); manager.append("thread-1", item_started("item-1")); manager.append("thread-1", item_delta("item-1")); let bootstrap = manager.subscribe("thread-1"); @@ -1205,18 +1183,18 @@ mod tests { .events; assert!(matches!( events.as_slice(), - [DirectThreadEvent::ItemCompleted { item, .. }] if item.item_id() == "item-1" + [ThreadEvent::ItemCompleted { item, .. }] if item.item_id() == "item-1" )); } #[test] fn slow_subscriber_is_expired_when_queue_limit_is_reached() { - let mut manager = DirectThreadManager::with_limits(2, 100_000); + let mut manager = ThreadManager::with_limits(2, 100_000); let subscription = manager.subscribe("thread-1"); for _ in 0..3 { manager.append( "thread-1", - request(DirectThreadRequestKind::RequestResolved, None), + request(ThreadRequestKind::RequestResolved, None), ); } assert_eq!( @@ -1227,7 +1205,7 @@ mod tests { #[test] fn current_subscriber_is_not_expired_by_pinned_queue_head() { - let mut manager = DirectThreadManager::with_limits(2, 100_000); + let mut manager = ThreadManager::with_limits(2, 100_000); manager.append("thread-1", item_started("item-1")); let subscription = manager.subscribe("thread-1"); for _ in 0..3 { @@ -1241,19 +1219,16 @@ mod tests { #[test] fn unresolved_approval_is_kept_in_bootstrap_until_resolved() { - let mut manager = DirectThreadManager::with_limits(100, 100_000); + let mut manager = ThreadManager::with_limits(100, 100_000); manager.append( "thread-1", - request( - DirectThreadRequestKind::ApprovalRequested, - Some("request-1"), - ), + request(ThreadRequestKind::ApprovalRequested, Some("request-1")), ); let bootstrap = manager.subscribe("thread-1"); assert_eq!(bootstrap.events.len(), 1); manager.append( "thread-1", - request(DirectThreadRequestKind::RequestResolved, Some("request-1")), + request(ThreadRequestKind::RequestResolved, Some("request-1")), ); assert_eq!( manager @@ -1267,18 +1242,15 @@ mod tests { #[test] fn resolved_request_releases_the_original_requested_event() { - let mut manager = DirectThreadManager::with_limits(100, 100_000); + let mut manager = ThreadManager::with_limits(100, 100_000); manager.append( "thread-1", - request( - DirectThreadRequestKind::ApprovalRequested, - Some("request-1"), - ), + request(ThreadRequestKind::ApprovalRequested, Some("request-1")), ); let subscription = manager.subscribe("thread-1"); manager.append( "thread-1", - request(DirectThreadRequestKind::RequestResolved, Some("request-1")), + request(ThreadRequestKind::RequestResolved, Some("request-1")), ); manager .consume(&subscription.subscription_id) @@ -1288,15 +1260,15 @@ mod tests { #[test] fn turn_completed_anchor_survives_empty_queue_for_new_subscriber() { - let mut manager = DirectThreadManager::with_limits(100, 100_000); + let mut manager = ThreadManager::with_limits(100, 100_000); manager.append( "thread-1", - DirectThreadEvent::turn_completed("completed".to_string(), FIXED_AT_MS), + ThreadEvent::turn_completed("completed".to_string(), FIXED_AT_MS), ); let bootstrap = manager.subscribe("thread-1"); assert!(matches!( bootstrap.events.as_slice(), - [DirectThreadEvent::TurnCompleted { status, at, .. }] + [ThreadEvent::TurnCompleted { status, at, .. }] if status == "completed" && *at == Some(FIXED_AT_MS) )); } @@ -1305,11 +1277,11 @@ mod tests { /// 成为锚点,否则队列被回收后新订阅只会看到 `turn.started`,把这轮已收口的回合重放成"还在跑"。 #[test] fn failed_turn_completed_replaces_started_anchor() { - let mut manager = DirectThreadManager::with_limits(100, 100_000); - manager.append("thread-1", DirectThreadEvent::turn_started(1_000)); + let mut manager = ThreadManager::with_limits(100, 100_000); + manager.append("thread-1", ThreadEvent::turn_started(1_000)); manager.append( "thread-1", - DirectThreadEvent::turn_completed_failed( + ThreadEvent::turn_completed_failed( crate::agent::DirectTurnFailure::new( crate::agent::DirectTurnFailureKind::HostDropped, "回合宿主任务提前结束", @@ -1321,7 +1293,7 @@ mod tests { let bootstrap = manager.subscribe("thread-1"); assert!(matches!( bootstrap.events.as_slice(), - [DirectThreadEvent::TurnCompleted { status, failure, at, .. }] + [ThreadEvent::TurnCompleted { status, failure, at, .. }] if status == "failed" && failure.as_ref().is_some_and(|failure| failure.kind == crate::agent::DirectTurnFailureKind::HostDropped) @@ -1333,20 +1305,16 @@ mod tests { /// 重放不得重新取钟(否则每次重连都会把已固定的起止时间改掉)。 #[test] fn replayed_events_keep_their_original_stage_time() { - let mut manager = DirectThreadManager::with_limits(100, 100_000); - manager.append("thread-1", DirectThreadEvent::turn_started(1_000)); + let mut manager = ThreadManager::with_limits(100, 100_000); + manager.append("thread-1", ThreadEvent::turn_started(1_000)); manager.append( "thread-1", - DirectThreadEvent::item_started(message("item-1"), 2_000), + ThreadEvent::item_started(message("item-1"), 2_000), ); let first = manager.subscribe("thread-1"); assert_eq!( - first - .events - .iter() - .map(DirectThreadEvent::at) - .collect::>(), + first.events.iter().map(ThreadEvent::at).collect::>(), vec![Some(1_000), Some(2_000)] ); @@ -1356,17 +1324,14 @@ mod tests { manager.append( "thread-1", - DirectThreadEvent::item_completed(message("item-1"), 3_000), + ThreadEvent::item_completed(message("item-1"), 3_000), ); let completion = manager .consume(&first.subscription_id) .expect("consume completion") .events; assert_eq!( - completion - .iter() - .map(DirectThreadEvent::at) - .collect::>(), + completion.iter().map(ThreadEvent::at).collect::>(), vec![Some(3_000)] ); // 重复消费不产生新事件,也不改写已下发过的时间。 @@ -1376,10 +1341,7 @@ mod tests { .events .is_empty()); assert_eq!( - completion - .iter() - .map(DirectThreadEvent::at) - .collect::>(), + completion.iter().map(ThreadEvent::at).collect::>(), vec![Some(3_000)] ); } @@ -1388,18 +1350,17 @@ mod tests { /// 的回合里也要能把边界认领给同一条用户条目,而不是按时间戳猜。 #[test] fn bootstrap_replays_opener_user_item_id() { - let mut manager = DirectThreadManager::with_limits(100, 100_000); + let mut manager = ThreadManager::with_limits(100, 100_000); manager.append( "thread-1", - DirectThreadEvent::turn_started(1_000) - .with_user_item_id(Some("direct-codex:turn-1:user")), + ThreadEvent::turn_started(1_000).with_user_item_id(Some("direct-codex:turn-1:user")), ); let bootstrap = manager.subscribe("thread-1"); assert_eq!( bootstrap .events .iter() - .map(DirectThreadEvent::user_item_id) + .map(ThreadEvent::user_item_id) .collect::>(), vec![Some("direct-codex:turn-1:user")] ); @@ -1411,11 +1372,11 @@ mod tests { .expect("consume anchor"); manager.append( "thread-1", - DirectThreadEvent::item_completed(message("item-1"), 2_000), + ThreadEvent::item_completed(message("item-1"), 2_000), ); manager.append( "thread-1", - DirectThreadEvent::turn_completed("completed".to_string(), 3_000) + ThreadEvent::turn_completed("completed".to_string(), 3_000) .with_user_item_id(Some("direct-codex:turn-1:user")), ); let second = manager.subscribe("thread-1"); @@ -1423,7 +1384,7 @@ mod tests { second .events .iter() - .map(DirectThreadEvent::user_item_id) + .map(ThreadEvent::user_item_id) .collect::>(), vec![Some("direct-codex:turn-1:user")], "起止同源:终态锚点也带同一个开口用户条目身份" @@ -1433,11 +1394,11 @@ mod tests { #[test] fn queue_cleanup_only_removes_a_cleanable_prefix() { - let mut manager = DirectThreadManager::with_limits(100, 100_000); + let mut manager = ThreadManager::with_limits(100, 100_000); manager.append("thread-1", item_started("item-1")); manager.append( "thread-1", - request(DirectThreadRequestKind::RequestResolved, None), + request(ThreadRequestKind::RequestResolved, None), ); let subscription = manager.subscribe("thread-1"); manager @@ -1451,10 +1412,7 @@ mod tests { } /// 首页快照就是逻辑回合的导出:放行即出现、进度按序号回填、收口即消失。 - fn snapshot_of( - manager: &DirectThreadManager, - thread_id: &str, - ) -> Option { + fn snapshot_of(manager: &ThreadManager, thread_id: &str) -> Option { manager .active_turn_snapshots() .into_iter() @@ -1463,7 +1421,7 @@ mod tests { #[test] fn active_turn_snapshot_follows_the_logical_turn_lifecycle() { - let mut manager = DirectThreadManager::with_limits(100, 100_000); + let mut manager = ThreadManager::with_limits(100, 100_000); let thread_id = "/tmp/快照项目"; assert!(snapshot_of(&manager, thread_id).is_none()); @@ -1507,13 +1465,13 @@ mod tests { manager.complete_turn( thread_id, - DirectThreadEvent::turn_completed("completed".to_string(), 5_000), + ThreadEvent::turn_completed("completed".to_string(), 5_000), ); assert!(snapshot_of(&manager, thread_id).is_none()); } - fn pending_turn(client_turn_id: &str) -> PendingDirectTurn { - PendingDirectTurn::prepare( + fn pending_turn(client_turn_id: &str) -> PendingTurn { + PendingTurn::prepare( client_turn_id.to_string(), serde_json::from_value(serde_json::json!({ "type": "message", @@ -1530,13 +1488,11 @@ mod tests { .expect("prepare pending turn") } - fn enqueued_ids(events: &[DirectThreadEvent]) -> Vec<&str> { + fn enqueued_ids(events: &[ThreadEvent]) -> Vec<&str> { events .iter() .filter_map(|event| match event { - DirectThreadEvent::QueueEnqueued { client_turn_id, .. } => { - Some(client_turn_id.as_str()) - } + ThreadEvent::QueueEnqueued { client_turn_id, .. } => Some(client_turn_id.as_str()), _ => None, }) .collect() @@ -1545,7 +1501,7 @@ mod tests { /// 队列就是事件列表:新订阅者的 bootstrap 看得见当前队列,取消之后那条就从队列里消失。 #[test] fn new_subscribers_see_the_current_queue_until_it_is_removed() { - let mut manager = DirectThreadManager::with_limits(100, 100_000); + let mut manager = ThreadManager::with_limits(100, 100_000); manager .enqueue_pending_turn("thread-1", pending_turn("turn-1")) .expect("enqueue head"); @@ -1560,7 +1516,7 @@ mod tests { let _ = manager.consume(&watcher.subscription_id).expect("drain"); assert_eq!( manager.remove_pending_turn("thread-1", "turn-1", FIXED_AT_MS), - DirectQueueRemovalOutcome::Removed + QueueRemovalOutcome::Removed ); // 离开队列的事件带 typed 原因:这条是用户取消。 let events = manager @@ -1570,8 +1526,8 @@ mod tests { assert!( matches!( events.as_slice(), - [DirectThreadEvent::QueueRemoved { client_turn_id, reason, .. }] - if client_turn_id == "turn-1" && *reason == DirectQueueRemovalReason::Cancelled + [ThreadEvent::QueueRemoved { client_turn_id, reason, .. }] + if client_turn_id == "turn-1" && *reason == QueueRemovalReason::Cancelled ), "{events:?}" ); @@ -1587,7 +1543,7 @@ mod tests { /// 排着的那几条还在队,而不是只剩其中一个。 #[test] fn late_subscriber_sees_the_running_turn_and_the_pending_queue_together() { - let mut manager = DirectThreadManager::with_limits(100, 100_000); + let mut manager = ThreadManager::with_limits(100, 100_000); manager .enqueue_pending_turn("thread-1", pending_turn("turn-1")) .expect("enqueue head"); @@ -1603,8 +1559,8 @@ mod tests { matches!( bootstrap.events.as_slice(), [ - DirectThreadEvent::TurnStarted { .. }, - DirectThreadEvent::QueueEnqueued { client_turn_id, .. } + ThreadEvent::TurnStarted { .. }, + ThreadEvent::QueueEnqueued { client_turn_id, .. } ] if client_turn_id == "turn-2" ), "中途加入者要先看到在跑的那一轮、再看到还排着的那几条:{:?}", @@ -1621,7 +1577,7 @@ mod tests { /// 入队幂等:判重范围是「在队 ∪ 正在跑的那一轮」,重复入队不排第二条、不发第二条事件。 #[test] fn enqueue_is_idempotent_per_client_turn_id() { - let mut manager = DirectThreadManager::with_limits(100, 100_000); + let mut manager = ThreadManager::with_limits(100, 100_000); assert_eq!( manager .enqueue_pending_turn("thread-1", pending_turn("turn-1")) @@ -1652,8 +1608,8 @@ mod tests { /// 容量只数在队条目:正在跑的那一轮不占等待位。 #[test] fn capacity_counts_only_pending_turns() { - let mut manager = DirectThreadManager::with_limits(100, 100_000); - for index in 0..MAX_PENDING_DIRECT_TURNS { + let mut manager = ThreadManager::with_limits(100, 100_000); + for index in 0..MAX_PENDING_TURNS { assert_eq!( manager .enqueue_pending_turn("thread-1", pending_turn(&format!("turn-{index}"))) @@ -1680,17 +1636,14 @@ mod tests { /// 入队侧拿来做容量预判的条数与权威判据同源:只数在队条目,认领 / 取消之后立刻跟着变。 #[test] fn pending_turn_count_tracks_only_the_queue() { - let mut manager = DirectThreadManager::with_limits(100, 100_000); + let mut manager = ThreadManager::with_limits(100, 100_000); assert_eq!(manager.pending_turn_count("thread-unknown"), 0); - for index in 0..MAX_PENDING_DIRECT_TURNS { + for index in 0..MAX_PENDING_TURNS { manager .enqueue_pending_turn("thread-1", pending_turn(&format!("turn-{index}"))) .expect("enqueue"); } - assert_eq!( - manager.pending_turn_count("thread-1"), - MAX_PENDING_DIRECT_TURNS - ); + assert_eq!(manager.pending_turn_count("thread-1"), MAX_PENDING_TURNS); assert_eq!( queue_has_room(manager.pending_turn_count("thread-1")), Err(EnqueueRejection::QueueFull) @@ -1702,19 +1655,19 @@ mod tests { .expect("claim head"); assert_eq!( manager.pending_turn_count("thread-1"), - MAX_PENDING_DIRECT_TURNS - 1 + MAX_PENDING_TURNS - 1 ); manager.remove_pending_turn("thread-1", "turn-1", FIXED_AT_MS); assert_eq!( manager.pending_turn_count("thread-1"), - MAX_PENDING_DIRECT_TURNS - 2 + MAX_PENDING_TURNS - 2 ); } /// 放行认领的是**队首**,而且开始事件与离开队列事件同批写:中间没有第二个中间态。 #[test] fn dispatch_claims_the_head_and_writes_both_events_in_one_batch() { - let mut manager = DirectThreadManager::with_limits(100, 100_000); + let mut manager = ThreadManager::with_limits(100, 100_000); manager .enqueue_pending_turn("thread-1", pending_turn("turn-1")) .expect("enqueue head"); @@ -1739,11 +1692,11 @@ mod tests { matches!( events.as_slice(), [ - DirectThreadEvent::TurnStarted { at, .. }, - DirectThreadEvent::QueueRemoved { client_turn_id, reason, at: removed_at }, + ThreadEvent::TurnStarted { at, .. }, + ThreadEvent::QueueRemoved { client_turn_id, reason, at: removed_at }, ] if *at == Some(2_000) && client_turn_id == "turn-1" - && *reason == DirectQueueRemovalReason::Dispatched + && *reason == QueueRemovalReason::Dispatched && *removed_at == Some(2_000) ), "{events:?}" @@ -1757,7 +1710,7 @@ mod tests { /// 兜底释放是**一个动作**:判据不过时占用与事件都不动,通过时占用解除与兜底终态一起落地。 #[test] fn stale_release_validates_and_releases_as_one_action() { - let mut manager = DirectThreadManager::with_limits(100, 100_000); + let mut manager = ThreadManager::with_limits(100, 100_000); manager .enqueue_pending_turn("thread-1", pending_turn("turn-1")) .expect("enqueue"); @@ -1770,12 +1723,12 @@ mod tests { // 身份对不上:放回去,占用不动、一个字都不写。 assert_eq!( manager.release_stale_turn("thread-1", Some("turn-other"), None, FIXED_AT_MS + 10_000), - DirectStaleTurnRelease::AnotherTurnRunning + StaleTurnRelease::AnotherTurnRunning ); // 启动窗口内:同样什么都不写。 assert_eq!( manager.release_stale_turn("thread-1", None, Some(60_000), FIXED_AT_MS + 1), - DirectStaleTurnRelease::WithinStartupWindow { age_ms: 1 } + StaleTurnRelease::WithinStartupWindow { age_ms: 1 } ); assert!(manager.turn_is_active("thread-1")); assert!( @@ -1795,7 +1748,7 @@ mod tests { Some(60_000), FIXED_AT_MS + 60_000 ), - DirectStaleTurnRelease::Released("turn-1".to_string()) + StaleTurnRelease::Released("turn-1".to_string()) ); assert!(!manager.turn_is_active("thread-1")); let events = manager @@ -1806,7 +1759,7 @@ mod tests { assert!( matches!( events[0], - DirectThreadEvent::TurnCompleted { ref status, .. } if status == "aborted" + ThreadEvent::TurnCompleted { ref status, .. } if status == "aborted" ), "{events:?}" ); @@ -1818,17 +1771,17 @@ mod tests { // 占用已经解除,再释放就是"没有活动回合"。 assert_eq!( manager.release_stale_turn("thread-1", None, None, FIXED_AT_MS + 60_000), - DirectStaleTurnRelease::NoActiveTurn + StaleTurnRelease::NoActiveTurn ); } /// 取消的三种结果分得开:真的移除了、已经被放行了、本来就不在队里。 #[test] fn removal_tells_cancelled_dispatched_and_unknown_apart() { - let mut manager = DirectThreadManager::with_limits(100, 100_000); + let mut manager = ThreadManager::with_limits(100, 100_000); assert_eq!( manager.remove_pending_turn("thread-1", "turn-1", FIXED_AT_MS), - DirectQueueRemovalOutcome::NotFound + QueueRemovalOutcome::NotFound ); manager @@ -1836,11 +1789,11 @@ mod tests { .expect("enqueue"); assert_eq!( manager.remove_pending_turn("thread-1", "turn-1", FIXED_AT_MS), - DirectQueueRemovalOutcome::Removed + QueueRemovalOutcome::Removed ); assert_eq!( manager.remove_pending_turn("thread-1", "turn-1", FIXED_AT_MS), - DirectQueueRemovalOutcome::NotFound + QueueRemovalOutcome::NotFound ); manager @@ -1851,7 +1804,7 @@ mod tests { .expect("claim head"); assert_eq!( manager.remove_pending_turn("thread-1", "turn-2", FIXED_AT_MS), - DirectQueueRemovalOutcome::AlreadyDispatched + QueueRemovalOutcome::AlreadyDispatched ); } @@ -1864,20 +1817,20 @@ mod tests { let thread_id = format!("/tmp/事件项目-{}", Uuid::new_v4()); let before = crate::agent::direct_active_turns_event_test_count(); - enqueue_direct_pending_turn(&thread_id, pending_turn("turn-1")).expect("enqueue"); + enqueue_pending_turn(&thread_id, pending_turn("turn-1")).expect("enqueue"); assert_eq!( crate::agent::direct_active_turns_event_test_count(), before, "入队只是排队,不进快照:不通知" ); - claim_direct_pending_turn(&thread_id).expect("放行队首"); + claim_pending_turn(&thread_id).expect("放行队首"); assert_eq!( crate::agent::direct_active_turns_event_test_count(), before + 1, "放行必须通知一次" ); - assert!(update_direct_thread_active_turn( + assert!(update_active_turn( &thread_id, "turn-1", "streaming", @@ -1888,7 +1841,7 @@ mod tests { let after_progress = crate::agent::direct_active_turns_event_test_count(); assert_eq!(after_progress, before + 2, "进度内容变化必须通知一次"); - assert!(!update_direct_thread_active_turn( + assert!(!update_active_turn( &thread_id, "turn-1", "streaming", @@ -1902,16 +1855,16 @@ mod tests { "状态与活动都没变的重复进度不重复通知" ); - complete_direct_thread_turn( + complete_turn( &thread_id, - DirectThreadEvent::turn_completed("completed".to_string(), 5_000), + ThreadEvent::turn_completed("completed".to_string(), 5_000), ); assert_eq!( crate::agent::direct_active_turns_event_test_count(), after_progress + 1, "收口必须通知一次" ); - assert!(list_direct_active_turns() + assert!(list_active_turns() .expect("list active turns") .into_iter() .all(|turn| turn.project_path != thread_id)); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_queue.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/queue.rs similarity index 91% rename from apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_queue.rs rename to apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/queue.rs index d06608f8b..b3c37f2b2 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_queue.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/queue.rs @@ -4,28 +4,28 @@ //! `queue.enqueued` 事件不可回收;离开队列(取消或放行)时才转成可回收。新订阅者的 bootstrap 因此 //! 天然看得见当前队列,不需要第二张队列表,也不会有"事件与队列不一致"的窗口。 //! -//! 这个模块只放三件事:一条待发消息带走什么([`PendingDirectTurn`])、容量规则 -//! ([`MAX_PENDING_DIRECT_TURNS`])、以及它在线上长什么样([`PendingDirectTurn::enqueued_event`])。 +//! 这个模块只放三件事:一条待发消息带走什么([`PendingTurn`])、容量规则 +//! ([`MAX_PENDING_TURNS`])、以及它在线上长什么样([`PendingTurn::enqueued_event`])。 //! 它不碰锁、不碰 Tauri、不写盘:入队检查在命令侧,放行顺序在 Thread Manager。 use serde_json::Value; use crate::agent::{ - direct_codex_user_item_id_for_client_turn_id, DirectCodexUserItem, DirectThreadEvent, + direct_codex_user_item_id_for_client_turn_id, DirectCodexUserItem, ThreadEvent, }; /// 一个项目最多能同时排队的待发消息条数。 /// /// 只数**在队**条目,不算正在跑的那一轮。上限只落在宿主这一处:前端不再自己数,满队由命令返回 /// typed 入队失败。 -pub(crate) const MAX_PENDING_DIRECT_TURNS: usize = 5; +pub(crate) const MAX_PENDING_TURNS: usize = 5; /// 一条已经通过入队检查、正在等放行的用户消息。 /// /// 只在内存里,进程重启即消失(与 ADR 记的边界一致)。它同时是**放行时要用的全部输入**:放行 /// 没有失败出口,所以检查产物在入队时就地冻结,放行只搬运、不重算。 #[derive(Clone, Debug)] -pub(crate) struct PendingDirectTurn { +pub(crate) struct PendingTurn { /// 这条消息的回合身份;放行后同一轮的 `turn.started` / `turn.completed` 用它。 pub(crate) client_turn_id: String, /// canonical 用户条目:事件与界面 chip 都读它,Rust 不渲染展示形状。 @@ -41,7 +41,7 @@ pub(crate) struct PendingDirectTurn { pub(crate) at: u64, } -impl PendingDirectTurn { +impl PendingTurn { /// 组一条待发消息:入队检查已经全部通过,这里只把放行要用的产物冻结下来。 /// /// 冻结是刻意的:放行没有失败出口,所以任何可能在放行时才失败的计算都必须提前到这里 @@ -68,8 +68,8 @@ impl PendingDirectTurn { /// 入队事件的投影:带 canonical 用户条目与 `creationType`,**不带 prompt**(prompt 只留在宿主的 /// 队列条目里,它不是要下发的展示形状)。 - pub(crate) fn enqueued_event(&self) -> DirectThreadEvent { - DirectThreadEvent::queue_enqueued( + pub(crate) fn enqueued_event(&self) -> ThreadEvent { + ThreadEvent::queue_enqueued( self.client_turn_id.clone(), self.user_item.clone(), self.creation_type.clone(), @@ -95,13 +95,13 @@ pub(crate) enum EnqueueOutcome { /// 入队被检查挡下来的原因。满队之外的原因由入队侧自己的 typed 错误表达。 #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum EnqueueRejection { - /// 在队条目已达 [`MAX_PENDING_DIRECT_TURNS`]。 + /// 在队条目已达 [`MAX_PENDING_TURNS`]。 QueueFull, } /// 队列还有没有位置。`pending_count` 只数在队条目。 pub(crate) fn queue_has_room(pending_count: usize) -> Result<(), EnqueueRejection> { - if pending_count >= MAX_PENDING_DIRECT_TURNS { + if pending_count >= MAX_PENDING_TURNS { return Err(EnqueueRejection::QueueFull); } Ok(()) @@ -122,8 +122,8 @@ mod tests { .expect("canonical user item") } - fn pending(client_turn_id: &str, creation_type: Option<&str>) -> PendingDirectTurn { - PendingDirectTurn::prepare( + fn pending(client_turn_id: &str, creation_type: Option<&str>) -> PendingTurn { + PendingTurn::prepare( client_turn_id.to_string(), user_item("生成一个游戏", "direct-codex:turn-1:user"), "生成一个游戏".to_string(), @@ -194,11 +194,11 @@ mod tests { /// 容量只数在队条目,5 条封口;在跑的那一轮不算进去。 #[test] fn capacity_closes_at_five_pending_turns() { - for count in 0..MAX_PENDING_DIRECT_TURNS { + for count in 0..MAX_PENDING_TURNS { assert_eq!(queue_has_room(count), Ok(()), "{count} 条时仍该有位置"); } assert_eq!( - queue_has_room(MAX_PENDING_DIRECT_TURNS), + queue_has_room(MAX_PENDING_TURNS), Err(EnqueueRejection::QueueFull) ); } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_wire.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/wire.rs similarity index 87% rename from apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_wire.rs rename to apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/wire.rs index 9140d232c..16ba49cfe 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_thread_wire.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/wire.rs @@ -22,17 +22,17 @@ use std::path::Path; use ts_rs::TS; /// 正文(消息 / 思考)上限。 -const DIRECT_THREAD_TEXT_MAX_CHARS: usize = 8_000; +const THREAD_TEXT_MAX_CHARS: usize = 8_000; /// 工具明细(命令 / 参数 / 输出)上限。 -const DIRECT_THREAD_DETAIL_MAX_CHARS: usize = 4_000; +const THREAD_DETAIL_MAX_CHARS: usize = 4_000; /// 单条变更路径上限。 -const DIRECT_THREAD_PATH_MAX_CHARS: usize = 300; +const THREAD_PATH_MAX_CHARS: usize = 300; /// 一条文件变更。 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] -pub(crate) struct DirectThreadFileChange { +pub(crate) struct ThreadFileChange { pub(crate) path: String, /// `add` | `update` | `delete` pub(crate) kind: String, @@ -41,14 +41,14 @@ pub(crate) struct DirectThreadFileChange { /// 聊天视图的输入条目:一条 Codex 原始条目的脱敏投影。 /// /// `itemType` 就是 Codex 的原始类型,逐字透传;前端按它决定投影成消息、思考还是工具卡片。 -/// 未识别的类型走 [`DirectThreadItem::Other`],Rust 不替前端决定它是否可见。 +/// 未识别的类型走 [`ThreadItem::Other`],Rust 不替前端决定它是否可见。 /// /// 条目上的 `at` 是只用于显示的毫秒时间戳:ts-rs 默认把 `u64` 映射成 `bigint`, /// 而 Tauri 的 JSON 通道传过来的是 `number`,因此统一标 `#[ts(as = "f64")]` 对齐。 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] #[serde(tag = "itemType", rename_all_fields = "camelCase", deny_unknown_fields)] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] -pub(crate) enum DirectThreadItem { +pub(crate) enum ThreadItem { #[serde(rename = "message")] Message { /// 归一身份:全链路只有这一个 id。 @@ -67,7 +67,7 @@ pub(crate) enum DirectThreadItem { at: u64, }, /// 原始 response item 的工具调用:参数在 `arguments`,输出在后续的 - /// [`DirectThreadItem::FunctionCallOutput`](两者共用归一身份)。 + /// [`ThreadItem::FunctionCallOutput`](两者共用归一身份)。 #[serde(rename = "function_call")] FunctionCall { item_id: String, @@ -101,7 +101,7 @@ pub(crate) enum DirectThreadItem { #[serde(rename = "fileChange")] FileChange { item_id: String, - changes: Vec, + changes: Vec, #[ts(as = "f64")] at: u64, }, @@ -143,7 +143,7 @@ pub(crate) enum DirectThreadItem { }, } -impl DirectThreadItem { +impl ThreadItem { /// 归一身份:Thread Manager 用它登记与释放未完成条目,前端用它合并同一张卡片。 pub(crate) fn item_id(&self) -> &str { match self { @@ -182,7 +182,7 @@ impl DirectThreadItem { #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] -pub(crate) enum DirectThreadDeltaKind { +pub(crate) enum ThreadDeltaKind { /// assistant 正文。 Message, /// 思考正文。 @@ -193,7 +193,7 @@ pub(crate) enum DirectThreadDeltaKind { #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] -pub(crate) enum DirectThreadRequestKind { +pub(crate) enum ThreadRequestKind { #[serde(rename = "approval.requested")] ApprovalRequested, #[serde(rename = "ask.requested")] @@ -202,7 +202,7 @@ pub(crate) enum DirectThreadRequestKind { RequestResolved, } -impl DirectThreadRequestKind { +impl ThreadRequestKind { /// 未解决的请求要留在 bootstrap 里,直到出现对应的解决事件。 pub(crate) fn is_request(&self) -> bool { matches!(self, Self::ApprovalRequested | Self::AskRequested) @@ -220,7 +220,7 @@ impl DirectThreadRequestKind { #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] -pub(crate) enum DirectQueueRemovalReason { +pub(crate) enum QueueRemovalReason { /// 用户取消了这条待发消息。 Cancelled, /// 放行:这条待发消息已经离开队列,成为正在跑的那一轮。 @@ -231,7 +231,7 @@ pub(crate) enum DirectQueueRemovalReason { #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] -pub(crate) enum DirectQueueRemovalOutcome { +pub(crate) enum QueueRemovalOutcome { /// 已从队列移除。 Removed, /// 这条消息已经被放行(正在跑的那一轮就是它),不能按待发消息取消。 @@ -277,7 +277,7 @@ impl DirectTurnFailure { /// `item.at` 只说明"这条条目什么时候被看到",工具计时不得拿它当开始或完成边界。 /// 条目阶段优先用通知层的毫秒字段(`startedAtMs` / `completedAtMs`),缺失才用宿主钟; /// 回合阶段没有可用的毫秒上游字段(Turn 只有秒级 `startedAt` / `completedAt`),一律用宿主 -/// 在该阶段取的毫秒钟——见 `direct_thread_turn_completed_at_ms` 的说明。 +/// 在该阶段取的毫秒钟——见 `thread_turn_completed_at_ms` 的说明。 /// `at` 在事件进入 Thread Manager 时就固定:重放(bootstrap / consume)必须沿用原值, /// 不能在前端收到或重放时重新取当前时间。 /// @@ -290,7 +290,7 @@ impl DirectTurnFailure { #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS)] #[serde(tag = "type", rename_all_fields = "camelCase", deny_unknown_fields)] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] -pub(crate) enum DirectThreadEvent { +pub(crate) enum ThreadEvent { #[serde(rename = "turn.started")] TurnStarted { /// 本轮开始的阶段时间(毫秒):**放行**那一刻的宿主毫秒钟(逻辑回合的起点,不是 @@ -323,7 +323,7 @@ pub(crate) enum DirectThreadEvent { }, #[serde(rename = "item.started")] ItemStarted { - item: DirectThreadItem, + item: ThreadItem, /// 条目开始执行的原生阶段时间(毫秒);缺失时是宿主观测到该阶段的时间。 #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional, as = "Option")] @@ -331,7 +331,7 @@ pub(crate) enum DirectThreadEvent { }, #[serde(rename = "item.completed")] ItemCompleted { - item: DirectThreadItem, + item: ThreadItem, /// 条目结束的原生阶段时间(毫秒);缺失时是宿主观测到该阶段的时间。 #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional, as = "Option")] @@ -340,12 +340,12 @@ pub(crate) enum DirectThreadEvent { #[serde(rename = "item.delta")] ItemDelta { item_id: String, - kind: DirectThreadDeltaKind, + kind: ThreadDeltaKind, delta: String, }, #[serde(rename = "request")] Request { - kind: DirectThreadRequestKind, + kind: ThreadRequestKind, #[serde(default)] request_id: Option, }, @@ -372,14 +372,14 @@ pub(crate) enum DirectThreadEvent { #[serde(rename = "queue.removed")] QueueRemoved { client_turn_id: String, - reason: DirectQueueRemovalReason, + reason: QueueRemovalReason, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional, as = "Option")] at: Option, }, } -impl DirectThreadEvent { +impl ThreadEvent { pub(crate) fn turn_started(at: u64) -> Self { Self::TurnStarted { at: Some(at), @@ -453,15 +453,15 @@ impl DirectThreadEvent { } } - pub(crate) fn item_started(item: DirectThreadItem, at: u64) -> Self { + pub(crate) fn item_started(item: ThreadItem, at: u64) -> Self { Self::ItemStarted { item, at: Some(at) } } - pub(crate) fn item_completed(item: DirectThreadItem, at: u64) -> Self { + pub(crate) fn item_completed(item: ThreadItem, at: u64) -> Self { Self::ItemCompleted { item, at: Some(at) } } - pub(crate) fn item_delta(item_id: String, kind: DirectThreadDeltaKind, delta: String) -> Self { + pub(crate) fn item_delta(item_id: String, kind: ThreadDeltaKind, delta: String) -> Self { Self::ItemDelta { item_id, kind, @@ -469,7 +469,7 @@ impl DirectThreadEvent { } } - pub(crate) fn request(kind: DirectThreadRequestKind, request_id: Option) -> Self { + pub(crate) fn request(kind: ThreadRequestKind, request_id: Option) -> Self { Self::Request { kind, request_id } } @@ -491,7 +491,7 @@ impl DirectThreadEvent { /// 待发消息离开队列事件。 pub(crate) fn queue_removed( client_turn_id: String, - reason: DirectQueueRemovalReason, + reason: QueueRemovalReason, at: u64, ) -> Self { Self::QueueRemoved { @@ -511,7 +511,7 @@ impl DirectThreadEvent { } /// 待发消息离开队列的原因:只有 `queue.removed` 有。 - pub(crate) fn queue_removal_reason(&self) -> Option { + pub(crate) fn queue_removal_reason(&self) -> Option { match self { Self::QueueRemoved { reason, .. } => Some(*reason), _ => None, @@ -551,7 +551,7 @@ impl DirectThreadEvent { } } - pub(crate) fn request_kind(&self) -> Option { + pub(crate) fn request_kind(&self) -> Option { match self { Self::Request { kind, .. } => Some(*kind), _ => None, @@ -562,28 +562,28 @@ impl DirectThreadEvent { #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] -pub(crate) struct DirectThreadSubscriptionBootstrap { +pub(crate) struct SubscriptionBootstrap { pub(crate) subscription_id: String, /// 首屏历史锚点:`project.jsonl` 里最后一条原始 item id。 #[serde(default)] pub(crate) last_completed_item_id: Option, /// 该 subscriber 此刻应当处理的运行态事件(游标已经在队尾)。 - pub(crate) events: Vec, + pub(crate) events: Vec, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] -pub(crate) struct DirectThreadConsumeResult { - pub(crate) events: Vec, +pub(crate) struct ConsumeResult { + pub(crate) events: Vec, } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))] -pub(crate) struct DirectThreadHistorySlice { +pub(crate) struct HistorySlice { /// 脱敏条目,顺序即文件顺序;与运行态事件里的条目同形。 - pub(crate) items: Vec, + pub(crate) items: Vec, pub(crate) has_more: bool, /// 本次切片的原始 item id 锚点:无论切片里有没有可显示条目,分页都靠它向前。 #[serde(default)] @@ -600,10 +600,7 @@ fn bounded(value: &str, max_chars: usize) -> String { } fn detail_text(root: &Path, value: &str) -> String { - bounded( - &sanitize_detail_text(root, value), - DIRECT_THREAD_DETAIL_MAX_CHARS, - ) + bounded(&sanitize_detail_text(root, value), THREAD_DETAIL_MAX_CHARS) } /// 原始值转文本:字符串原样,其它 JSON 值序列化(调用方随后脱敏)。 @@ -639,16 +636,13 @@ fn item_text(root: &Path, item: &Value) -> Option { .filter(|text| !text.trim().is_empty())?; Some(bounded( &sanitize_detail_text(root, &raw), - DIRECT_THREAD_TEXT_MAX_CHARS, + THREAD_TEXT_MAX_CHARS, )) } /// 流式正文与完成态条目使用同一套脱敏和字符预算,避免增量文本绕过历史投影的安全边界。 -pub(crate) fn direct_thread_delta_text(root: &Path, value: &str) -> String { - bounded( - &sanitize_detail_text(root, value), - DIRECT_THREAD_TEXT_MAX_CHARS, - ) +pub(crate) fn thread_delta_text(root: &Path, value: &str) -> String { + bounded(&sanitize_detail_text(root, value), THREAD_TEXT_MAX_CHARS) } fn item_at_ms(item: &Value, observed_at_ms: u64) -> u64 { @@ -684,7 +678,7 @@ fn json_ms(container: &Value, key: &str) -> Option { /// 条目自带时用条目里的同名毫秒字段(`direct_tool_calls` 读的是同一处)。完成事件即使同时 /// 带着开始字段也只取**完成**时间;两者都没有、但 `durationMs` 有可靠起点时按 /// 起点 + 时长派生结束。都没有就用宿主处理该事件的钟——原生缺阶段时间时这是唯一诚实的值。 -pub(crate) fn direct_thread_item_event_at_ms( +pub(crate) fn thread_item_event_at_ms( params: &Value, item: &Value, completed: bool, @@ -716,7 +710,7 @@ pub(crate) fn direct_thread_item_event_at_ms( /// /// `high_precision_started_at_ms` 是本轮开始时宿主记下的那个毫秒起点(即 `turn.started` /// 事件写入的同一个值),不是从上游秒字段换算出来的,`None` 表示起点也不可证明。 -pub(crate) fn direct_thread_turn_completed_at_ms( +pub(crate) fn thread_turn_completed_at_ms( turn: &Value, high_precision_started_at_ms: Option, observed_at_ms: u64, @@ -728,7 +722,7 @@ pub(crate) fn direct_thread_turn_completed_at_ms( } /// 归一身份:工具条目用工具调用 id,其它条目用自己的 `id`;只产出这一个值。 -pub(crate) fn direct_thread_item_identity(item: &Value) -> Option { +pub(crate) fn thread_item_identity(item: &Value) -> Option { let call_id = item .get("call_id") .or_else(|| item.get("callId")) @@ -745,7 +739,7 @@ pub(crate) fn direct_thread_item_identity(item: &Value) -> Option { call_id.or(id) } -fn item_changes(root: &Path, item: &Value) -> Vec { +fn item_changes(root: &Path, item: &Value) -> Vec { item.get("changes") .and_then(Value::as_array) .map(|changes| { @@ -757,11 +751,8 @@ fn item_changes(root: &Path, item: &Value) -> Vec { .and_then(Value::as_str) .map(str::trim) .filter(|path| !path.is_empty())?; - Some(DirectThreadFileChange { - path: bounded( - &sanitize_detail_text(root, path), - DIRECT_THREAD_PATH_MAX_CHARS, - ), + Some(ThreadFileChange { + path: bounded(&sanitize_detail_text(root, path), THREAD_PATH_MAX_CHARS), kind: change .get("kind") .and_then(Value::as_str) @@ -783,15 +774,15 @@ fn field_text(root: &Path, item: &Value, key: &str) -> Option { /// 把一条 Codex 原始条目投影成线上条目;拿不到身份或类型时返回 `None`。 /// /// `observed_at_ms` 只在条目自带时间缺失时兜底(运行态用当前时间,历史用文件记录时间)。 -pub(crate) fn direct_thread_item_from_value( +pub(crate) fn thread_item_from_value( root: &Path, item: &Value, observed_at_ms: u64, -) -> Option { +) -> Option { if !item.is_object() { return None; } - let item_id = direct_thread_item_identity(item)?; + let item_id = thread_item_identity(item)?; let item_type = item .get("type") .and_then(Value::as_str) @@ -807,7 +798,7 @@ pub(crate) fn direct_thread_item_from_value( .map(str::to_string); Some(match item_type { - "message" | "agentMessage" | "userMessage" => DirectThreadItem::Message { + "message" | "agentMessage" | "userMessage" => ThreadItem::Message { item_id, role: role.unwrap_or_else(|| { if item_type == "userMessage" { @@ -819,12 +810,12 @@ pub(crate) fn direct_thread_item_from_value( text: text?, at, }, - "reasoning" => DirectThreadItem::Reasoning { + "reasoning" => ThreadItem::Reasoning { item_id, text: text?, at, }, - "function_call" => DirectThreadItem::FunctionCall { + "function_call" => ThreadItem::FunctionCall { item_id, name: item .get("name") @@ -834,12 +825,12 @@ pub(crate) fn direct_thread_item_from_value( arguments: field_text(root, item, "arguments").unwrap_or_default(), at, }, - "function_call_output" => DirectThreadItem::FunctionCallOutput { + "function_call_output" => ThreadItem::FunctionCallOutput { item_id, output: field_text(root, item, "output").unwrap_or_default(), at, }, - "commandExecution" => DirectThreadItem::CommandExecution { + "commandExecution" => ThreadItem::CommandExecution { item_id, command: item .get("command") @@ -858,12 +849,12 @@ pub(crate) fn direct_thread_item_from_value( exit_code: item.get("exitCode").and_then(Value::as_i64), at, }, - "fileChange" => DirectThreadItem::FileChange { + "fileChange" => ThreadItem::FileChange { item_id, changes: item_changes(root, item), at, }, - "mcpToolCall" => DirectThreadItem::McpToolCall { + "mcpToolCall" => ThreadItem::McpToolCall { item_id, tool: item .get("tool") @@ -882,7 +873,7 @@ pub(crate) fn direct_thread_item_from_value( .map(str::to_string), at, }, - "webSearch" => DirectThreadItem::WebSearch { + "webSearch" => ThreadItem::WebSearch { item_id, query: field_text(root, item, "query").or_else(|| { item.get("action") @@ -893,8 +884,8 @@ pub(crate) fn direct_thread_item_from_value( output: field_text(root, item, "output"), at, }, - "contextCompaction" => DirectThreadItem::ContextCompaction { item_id, at }, - other => DirectThreadItem::Other { + "contextCompaction" => ThreadItem::ContextCompaction { item_id, at }, + other => ThreadItem::Other { item_id, raw_type: other.to_string(), at, @@ -905,14 +896,14 @@ pub(crate) fn direct_thread_item_from_value( /// 历史切片投影:保持文件顺序,不做任何合并(同一调用的调用与输出是两条条目)。 /// /// `timestamp_of` 是文件记录时间,仅在条目自带时间缺失时兜底。 -pub(crate) fn direct_thread_items_from_history( +pub(crate) fn thread_items_from_history( root: &Path, items: &[Value], timestamp_of: impl Fn(&Value) -> u64, -) -> Vec { +) -> Vec { items .iter() - .filter_map(|item| direct_thread_item_from_value(root, item, timestamp_of(item))) + .filter_map(|item| thread_item_from_value(root, item, timestamp_of(item))) .collect() } @@ -1047,7 +1038,7 @@ mod tests { #[test] fn message_item_carries_role_text_and_turn() { - let item = direct_thread_item_from_value( + let item = thread_item_from_value( root(), &json!({ "id": "direct-codex:turn-1:user", @@ -1060,7 +1051,7 @@ mod tests { .expect("user item"); assert_eq!( item, - DirectThreadItem::Message { + ThreadItem::Message { item_id: "direct-codex:turn-1:user".to_string(), role: "user".to_string(), text: "做一个拼图游戏".to_string(), @@ -1071,7 +1062,7 @@ mod tests { #[test] fn app_server_agent_message_defaults_to_assistant_role() { - let item = direct_thread_item_from_value( + let item = thread_item_from_value( root(), &json!({"id": "msg-1", "type": "agentMessage", "text": "已执行"}), 1000, @@ -1079,13 +1070,13 @@ mod tests { .expect("agent message"); assert!(matches!( item, - DirectThreadItem::Message { role, .. } if role == "assistant" + ThreadItem::Message { role, .. } if role == "assistant" )); } #[test] fn tool_item_identity_is_normalized_to_one_id() { - let item = direct_thread_item_from_value( + let item = thread_item_from_value( root(), &json!({ "id": "05dc0af1-8023-47fd-ad22-d54df2837b1b", @@ -1102,14 +1093,14 @@ mod tests { assert_eq!(item.item_id(), "call_00_Gpd0s0Ytm9YgIbwbEXva1473"); assert!(matches!( item, - DirectThreadItem::FunctionCall { name, arguments, .. } + ThreadItem::FunctionCall { name, arguments, .. } if name == "exec_command" && arguments == "{\"cmd\": \"ls\"}" )); } #[test] fn command_execution_keeps_raw_status_and_exit_code() { - let item = direct_thread_item_from_value( + let item = thread_item_from_value( root(), &json!({ "id": "call-1", @@ -1124,7 +1115,7 @@ mod tests { .expect("command item"); assert!(matches!( item, - DirectThreadItem::CommandExecution { + ThreadItem::CommandExecution { command, output: Some(output), status: Some(status), @@ -1136,7 +1127,7 @@ mod tests { #[test] fn secrets_and_absolute_paths_are_not_leaked() { - let item = direct_thread_item_from_value( + let item = thread_item_from_value( root(), &json!({ "id": "msg-1", @@ -1147,7 +1138,7 @@ mod tests { 0, ) .expect("assistant item"); - let DirectThreadItem::Message { text, .. } = item else { + let ThreadItem::Message { text, .. } = item else { panic!("message item"); }; assert!( @@ -1174,15 +1165,12 @@ mod tests { "output": "assets\ngame\n", }), ]; - let projected = direct_thread_items_from_history(root(), &items, |_| 0); + let projected = thread_items_from_history(root(), &items, |_| 0); assert_eq!(projected.len(), 2, "搬运层不得替前端做合并"); - assert!(matches!( - projected[0], - DirectThreadItem::FunctionCall { .. } - )); + assert!(matches!(projected[0], ThreadItem::FunctionCall { .. })); assert!(matches!( projected[1], - DirectThreadItem::FunctionCallOutput { .. } + ThreadItem::FunctionCallOutput { .. } )); // 调用与输出共享同一个归一身份,前端才能把它们并成一张卡片。 assert_eq!(projected[0].item_id(), projected[1].item_id()); @@ -1191,7 +1179,7 @@ mod tests { #[test] fn unknown_item_types_are_passed_through_without_body() { - let item = direct_thread_item_from_value( + let item = thread_item_from_value( root(), &json!({"id": "plan-1", "type": "plan", "text": "内部计划"}), 0, @@ -1201,7 +1189,7 @@ mod tests { item, // TODO(direct-thread): 未知类型目前只带类型与身份,前端投影会丢弃它。 // 哪些类型要显示属于前端可见性决策,需要时改前端,不要在这里加白名单。 - DirectThreadItem::Other { ref raw_type, .. } if raw_type == "plan" + ThreadItem::Other { ref raw_type, .. } if raw_type == "plan" )); } @@ -1218,11 +1206,11 @@ mod tests { "startedAtMs": 1_000u64, }, }); - let item = direct_thread_item_from_value(root(), ¶ms["item"], 7_777).expect("item"); + let item = thread_item_from_value(root(), ¶ms["item"], 7_777).expect("item"); // 条目展示时间不受事件级时间影响,仍按条目自己的字段推导。 assert_eq!(item.at(), 1_000); assert_eq!( - direct_thread_item_event_at_ms(¶ms, ¶ms["item"], true, 7_777), + thread_item_event_at_ms(¶ms, ¶ms["item"], true, 7_777), 2_000 ); } @@ -1241,7 +1229,7 @@ mod tests { }, }); assert_eq!( - direct_thread_item_event_at_ms(¶ms, ¶ms["item"], false, 9_999), + thread_item_event_at_ms(¶ms, ¶ms["item"], false, 9_999), 1_700_000_000_123 ); } @@ -1256,18 +1244,18 @@ mod tests { }, }); assert_eq!( - direct_thread_item_event_at_ms(&nested, &nested["item"], false, 9_999), + thread_item_event_at_ms(&nested, &nested["item"], false, 9_999), 1_700_000_000_500 ); // 原生没有任何阶段时间:用宿主处理这条事件的钟,不编造。 let bare = json!({"item": {"id": "call-1", "type": "commandExecution"}}); assert_eq!( - direct_thread_item_event_at_ms(&bare, &bare["item"], false, 9_999), + thread_item_event_at_ms(&bare, &bare["item"], false, 9_999), 9_999 ); assert_eq!( - direct_thread_item_event_at_ms(&bare, &bare["item"], true, 9_999), + thread_item_event_at_ms(&bare, &bare["item"], true, 9_999), 9_999 ); } @@ -1282,7 +1270,7 @@ mod tests { "item": {"id": "call-1", "type": "commandExecution"}, }); assert_eq!( - direct_thread_item_event_at_ms(¶ms, ¶ms["item"], true, 9_999), + thread_item_event_at_ms(¶ms, ¶ms["item"], true, 9_999), 2_000 ); @@ -1296,7 +1284,7 @@ mod tests { }, }); assert_eq!( - direct_thread_item_event_at_ms(&nested, &nested["item"], true, 9_999), + thread_item_event_at_ms(&nested, &nested["item"], true, 9_999), 2_500 ); } @@ -1307,7 +1295,7 @@ mod tests { "item": {"id": "call-1", "type": "commandExecution", "startedAtMs": 1_000u64, "durationMs": 250u64}, }); assert_eq!( - direct_thread_item_event_at_ms(&with_start, &with_start["item"], true, 9_999), + thread_item_event_at_ms(&with_start, &with_start["item"], true, 9_999), 1_250 ); @@ -1316,7 +1304,7 @@ mod tests { "item": {"id": "call-1", "type": "commandExecution", "durationMs": 250u64}, }); assert_eq!( - direct_thread_item_event_at_ms(&duration_only, &duration_only["item"], true, 9_999), + thread_item_event_at_ms(&duration_only, &duration_only["item"], true, 9_999), 9_999 ); } @@ -1333,12 +1321,12 @@ mod tests { "completedAt": 1_700_000_042i64, }); assert_eq!( - direct_thread_turn_completed_at_ms(&seconds_only, Some(1_700_000_000_500), 9_999), + thread_turn_completed_at_ms(&seconds_only, Some(1_700_000_000_500), 9_999), 9_999, "没有 durationMs 时用宿主钟,不换算秒字段" ); assert_eq!( - direct_thread_turn_completed_at_ms(&seconds_only, None, 9_999), + thread_turn_completed_at_ms(&seconds_only, None, 9_999), 9_999 ); } @@ -1354,18 +1342,18 @@ mod tests { }); // 高精度起点(宿主在本轮开始时记下的毫秒值)+ 上游 durationMs:结束严格晚于起点。 assert_eq!( - direct_thread_turn_completed_at_ms(&with_duration, Some(1_700_000_000_500), 9_999), + thread_turn_completed_at_ms(&with_duration, Some(1_700_000_000_500), 9_999), 1_700_000_043_000 ); // 起点不可证明时不派生。 assert_eq!( - direct_thread_turn_completed_at_ms(&with_duration, None, 9_999), + thread_turn_completed_at_ms(&with_duration, None, 9_999), 9_999 ); // 时长为 0 同样按缺失处理。 let zero_duration = json!({"durationMs": 0u64}); assert_eq!( - direct_thread_turn_completed_at_ms(&zero_duration, Some(1_000), 9_999), + thread_turn_completed_at_ms(&zero_duration, Some(1_000), 9_999), 9_999 ); } @@ -1374,28 +1362,26 @@ mod tests { /// 反序列化仍成立,且不会序列化出 `at: null`。 #[test] fn lifecycle_events_serialize_event_level_at_as_optional_number() { - let started = serde_json::to_value(DirectThreadEvent::turn_started(1_700_000_000_123)) + let started = serde_json::to_value(ThreadEvent::turn_started(1_700_000_000_123)) .expect("serialize turn.started"); assert_eq!( started, json!({"type": "turn.started", "at": 1_700_000_000_123u64}) ); assert_eq!( - serde_json::from_value::(started).expect("round trip"), - DirectThreadEvent::turn_started(1_700_000_000_123) + serde_json::from_value::(started).expect("round trip"), + ThreadEvent::turn_started(1_700_000_000_123) ); - let completed = serde_json::to_value(DirectThreadEvent::turn_completed( - "completed".to_string(), - 2_000, - )) - .expect("serialize turn.completed"); + let completed = + serde_json::to_value(ThreadEvent::turn_completed("completed".to_string(), 2_000)) + .expect("serialize turn.completed"); assert_eq!( completed, json!({"type": "turn.completed", "status": "completed", "at": 2_000u64}) ); - let item = DirectThreadItem::CommandExecution { + let item = ThreadItem::CommandExecution { item_id: "call-1".to_string(), command: "ls".to_string(), output: None, @@ -1403,22 +1389,21 @@ mod tests { exit_code: None, at: 1_500, }; - let item_started = - serde_json::to_value(DirectThreadEvent::item_started(item.clone(), 1_000)) - .expect("serialize item.started"); + let item_started = serde_json::to_value(ThreadEvent::item_started(item.clone(), 1_000)) + .expect("serialize item.started"); assert_eq!(item_started["at"], json!(1_000u64)); // 事件级 `at` 不动条目自己的展示时间。 assert_eq!(item_started["item"]["at"], json!(1_500u64)); - let item_completed = serde_json::to_value(DirectThreadEvent::item_completed(item, 2_000)) + let item_completed = serde_json::to_value(ThreadEvent::item_completed(item, 2_000)) .expect("serialize item.completed"); assert_eq!(item_completed["at"], json!(2_000u64)); // 历史 / 夹具里的旧事件没有 `at`:反序列化成 `None`,回写时不补 `null`。 - let legacy: DirectThreadEvent = serde_json::from_value(json!({"type": "turn.started"})) + let legacy: ThreadEvent = serde_json::from_value(json!({"type": "turn.started"})) .expect("legacy turn.started without at"); assert_eq!( legacy, - DirectThreadEvent::TurnStarted { + ThreadEvent::TurnStarted { at: None, user_item_id: None, } @@ -1430,8 +1415,8 @@ mod tests { json!({"type": "turn.started"}) ); assert_eq!( - serde_json::to_value(DirectThreadEvent::request( - DirectThreadRequestKind::RequestResolved, + serde_json::to_value(ThreadEvent::request( + ThreadRequestKind::RequestResolved, None, )) .expect("serialize request"), @@ -1443,8 +1428,8 @@ mod tests { /// 缺省不写字段,旧事件反序列化仍是 `None`,空白身份按缺失处理(不猜)。 #[test] fn lifecycle_events_carry_optional_opener_user_item_id() { - let started = DirectThreadEvent::turn_started(1_000) - .with_user_item_id(Some("direct-codex:turn-1:user")); + let started = + ThreadEvent::turn_started(1_000).with_user_item_id(Some("direct-codex:turn-1:user")); assert_eq!(started.user_item_id(), Some("direct-codex:turn-1:user")); assert_eq!( serde_json::to_value(&started).expect("serialize turn.started"), @@ -1455,14 +1440,14 @@ mod tests { }) ); assert_eq!( - serde_json::from_value::( + serde_json::from_value::( serde_json::to_value(&started).expect("serialize") ) .expect("round trip"), started ); - let completed = DirectThreadEvent::turn_completed("interrupted".to_string(), 2_000) + let completed = ThreadEvent::turn_completed("interrupted".to_string(), 2_000) .with_user_item_id(Some("direct-codex:turn-1:user")); assert_eq!(completed.user_item_id(), Some("direct-codex:turn-1:user")); assert_eq!( @@ -1479,7 +1464,7 @@ mod tests { // 空白 / 空串按缺失处理:不能把 "" 当成一条用户条目的身份发下去。 for empty in ["", " "] { - let event = DirectThreadEvent::turn_started(1_000).with_user_item_id(Some(empty)); + let event = ThreadEvent::turn_started(1_000).with_user_item_id(Some(empty)); assert_eq!(event.user_item_id(), None); assert_eq!( serde_json::to_value(&event).expect("serialize"), @@ -1488,7 +1473,7 @@ mod tests { } // 旧事件(没有 `userItemId`)反序列化成 `None`,回写不补 `null`。 - let legacy: DirectThreadEvent = serde_json::from_value(json!({ + let legacy: ThreadEvent = serde_json::from_value(json!({ "type": "turn.completed", "status": "completed", "at": 3_000u64, @@ -1501,8 +1486,8 @@ mod tests { ); // 条目事件没有这个字段:身份只在生命周期事件上。 - let item_event = DirectThreadEvent::item_completed( - DirectThreadItem::CommandExecution { + let item_event = ThreadEvent::item_completed( + ThreadItem::CommandExecution { item_id: "call-1".to_string(), command: "ls".to_string(), output: None, @@ -1519,7 +1504,7 @@ mod tests { /// 一起流转,缺载荷的 `failed` 事件仍能反序列化(前端按"没有原因"处理,不猜)。 #[test] fn turn_completed_carries_failure_payload_only_when_failed() { - let failed = DirectThreadEvent::turn_completed_failed( + let failed = ThreadEvent::turn_completed_failed( DirectTurnFailure::new( crate::agent::DirectTurnFailureKind::ModelFailed, "上游返回 500:模型服务暂不可用", @@ -1546,7 +1531,7 @@ mod tests { }) ); assert_eq!( - serde_json::from_value::( + serde_json::from_value::( serde_json::to_value(&failed).expect("serialize") ) .expect("round trip"), @@ -1554,7 +1539,7 @@ mod tests { ); // 正常终态不带载荷,也不回写 `failure: null`。 - let completed = DirectThreadEvent::turn_completed("completed".to_string(), 5_000); + let completed = ThreadEvent::turn_completed("completed".to_string(), 5_000); assert_eq!(completed.failure(), None); assert_eq!( serde_json::to_value(&completed).expect("serialize completed turn"), @@ -1562,7 +1547,7 @@ mod tests { ); // 精简 / 旧形状:`failed` 但没有载荷也要能反序列化。 - let sparse: DirectThreadEvent = serde_json::from_value(json!({ + let sparse: ThreadEvent = serde_json::from_value(json!({ "type": "turn.completed", "status": "failed", })) @@ -1574,11 +1559,8 @@ mod tests { /// 不解析字符串。`at` 缺省时反序列化仍是 `None`。 #[test] fn queue_removed_carries_a_typed_reason() { - let dispatched = DirectThreadEvent::queue_removed( - "turn-1".to_string(), - DirectQueueRemovalReason::Dispatched, - 2_000, - ); + let dispatched = + ThreadEvent::queue_removed("turn-1".to_string(), QueueRemovalReason::Dispatched, 2_000); assert_eq!( serde_json::to_value(&dispatched).expect("serialize queue.removed"), json!({ @@ -1589,7 +1571,7 @@ mod tests { }) ); assert_eq!( - serde_json::from_value::( + serde_json::from_value::( serde_json::to_value(&dispatched).expect("serialize") ) .expect("round trip"), @@ -1598,11 +1580,11 @@ mod tests { assert_eq!(dispatched.queue_client_turn_id(), Some("turn-1")); assert_eq!( dispatched.queue_removal_reason(), - Some(DirectQueueRemovalReason::Dispatched) + Some(QueueRemovalReason::Dispatched) ); // 未识别的取值必须失败关闭:队列归属是宿主事实,不能让界面猜。 - serde_json::from_value::(json!({ + serde_json::from_value::(json!({ "type": "queue.removed", "clientTurnId": "turn-1", "reason": "timeout", @@ -1610,7 +1592,7 @@ mod tests { .expect_err("unknown queue removal reason must fail closed"); // 没有 `at` 的老形状仍能反序列化,回写不补 `null`。 - let legacy: DirectThreadEvent = serde_json::from_value(json!({ + let legacy: ThreadEvent = serde_json::from_value(json!({ "type": "queue.removed", "clientTurnId": "turn-1", "reason": "cancelled", diff --git a/apps/ai-game-creator-shell/src-tauri/src/commands.rs b/apps/ai-game-creator-shell/src-tauri/src/commands.rs index 08730c257..072750eca 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/commands.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/commands.rs @@ -5893,9 +5893,8 @@ pub(crate) async fn read_direct_project_conversation( } #[tauri::command] -pub(crate) fn list_game_creator_direct_active_turns( -) -> Result, String> { - list_direct_active_turns() +pub(crate) fn list_game_creator_direct_active_turns() -> Result, String> { + list_active_turns() } /// 取消一条**还没放行**的待发消息。 @@ -5907,15 +5906,12 @@ pub(crate) fn list_game_creator_direct_active_turns( pub(crate) async fn remove_direct_project_pending_turn( project_path: String, client_turn_id: String, -) -> Result { +) -> Result { tauri::async_runtime::spawn_blocking(move || { let root = Path::new(project_path.trim()); enforce_project_permission_policy(root, "conversation.write")?; - let thread_id = direct_thread_id_for_project(root); - Ok(remove_direct_pending_turn( - &thread_id, - client_turn_id.trim(), - )) + let thread_id = thread_id_for_project(root); + Ok(remove_pending_turn(&thread_id, client_turn_id.trim())) }) .await .map_err(|error| format!("取消 DirectProject 待发消息后台任务失败:{error}"))? @@ -5924,12 +5920,12 @@ pub(crate) async fn remove_direct_project_pending_turn( #[tauri::command] pub(crate) async fn subscribe_direct_project_thread( project_path: String, -) -> Result { +) -> Result { tauri::async_runtime::spawn_blocking(move || { let root = Path::new(project_path.trim()); enforce_project_permission_policy(root, "conversation.read")?; - let thread_id = direct_thread_id_for_project(root); - let mut bootstrap = subscribe_direct_thread(&thread_id); + let thread_id = thread_id_for_project(root); + let mut bootstrap = subscribe_thread(&thread_id); if bootstrap.last_completed_item_id.is_none() { match read_direct_project_last_item_id_at(root) { Ok(last_completed_item_id) => { @@ -5938,7 +5934,7 @@ pub(crate) async fn subscribe_direct_project_thread( Err(error) => { // 订阅已经登记,后续锚点读取失败时也必须回收 native // subscriber;否则前端拿不到 subscriptionId,无法自行清理。 - unsubscribe_direct_thread(&bootstrap.subscription_id); + unsubscribe_thread(&bootstrap.subscription_id); return Err(error); } } @@ -5952,13 +5948,13 @@ pub(crate) async fn subscribe_direct_project_thread( #[tauri::command] pub(crate) fn consume_direct_project_thread( subscription_id: String, -) -> Result { - consume_direct_thread(subscription_id.trim()) +) -> Result { + consume_thread(subscription_id.trim()) } #[tauri::command] pub(crate) fn unsubscribe_direct_project_thread(subscription_id: String) -> Result<(), String> { - unsubscribe_direct_thread(subscription_id.trim()); + unsubscribe_thread(subscription_id.trim()); Ok(()) } @@ -5973,7 +5969,7 @@ pub(crate) async fn read_direct_project_history_slice( before_item_id: Option, through_item_id: Option, limit: Option, -) -> Result { +) -> Result { tauri::async_runtime::spawn_blocking(move || { let root = Path::new(project_path.trim()); enforce_project_permission_policy(root, "conversation.read")?; @@ -5990,12 +5986,12 @@ pub(crate) async fn read_direct_project_history_slice( }; let (raw_items, has_more, recorded_at_ms, first_item_id) = read_direct_project_history_items_slice_at(root, anchor, limit.unwrap_or(20))?; - let items = direct_thread_items_from_history(root, &raw_items, |item| { - direct_thread_item_identity(item) + let items = thread_items_from_history(root, &raw_items, |item| { + thread_item_identity(item) .and_then(|identity| recorded_at_ms.get(&identity).copied()) .unwrap_or_default() }); - Ok(DirectThreadHistorySlice { + Ok(HistorySlice { items, has_more, first_item_id, diff --git a/apps/ai-game-creator-shell/src-tauri/src/main.rs b/apps/ai-game-creator-shell/src-tauri/src/main.rs index e7bc8aaa8..00031961f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -2565,7 +2565,7 @@ fn main() { })?; setup_log.append("startup.runner.start.begin"); set_game_creator_agent_runtime_update_app_handle(app.handle().clone()); - set_direct_thread_manager_app_handle(app.handle().clone()); + set_thread_manager_app_handle(app.handle().clone()); platform_maintenance::set_platform_maintenance_app_handle(app.handle().clone()); auth_session::initialize_auth_session(app.handle()); set_asset_generation_task_app_handle(app.handle().clone()); diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts index 97223c40a..c9c607f97 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts @@ -35,9 +35,9 @@ import { toDirectCodexTurnAttachments, } from '../conversation/directCodexTurnAttachments'; import type { DirectCodexUserContentPart } from '../generated/DirectCodexUserContentPart'; -import type { DirectQueueRemovalOutcome } from '../generated/DirectQueueRemovalOutcome'; -import type { DirectThreadHistorySlice } from '../generated/DirectThreadHistorySlice'; -import type { DirectThreadItem } from '../generated/DirectThreadItem'; +import type { HistorySlice } from '../generated/HistorySlice'; +import type { QueueRemovalOutcome } from '../generated/QueueRemovalOutcome'; +import type { ThreadItem } from '../generated/ThreadItem'; import { directHistoryAnchorGateToWaitFor } from '../history/directHistoryAnchorGate'; import { readDirectHistoryPages } from '../history/directHistoryPaging'; import { canStopDirectProjectTurn } from './useDirectProjectTurnStatus'; @@ -324,7 +324,7 @@ export function useDirectProjectChatController({ return; } try { - const outcome = await invoke( + const outcome = await invoke( 'remove_direct_project_pending_turn', { projectPath: nextProjectPath, clientTurnId }, ); @@ -679,7 +679,7 @@ export function useDirectProjectChatController({ } function mergeHistoryPages(input: { - items: DirectThreadItem[]; + items: ThreadItem[]; hasMore: boolean; firstItemId: string | null; }) { @@ -709,18 +709,15 @@ export function useDirectProjectChatController({ existingEntries: [], beforeItemId: null, readSlice: (beforeItemId) => - invoke( - 'read_direct_project_history_slice', - { - projectPath: nextProjectPath, - ...(beforeItemId - ? { beforeItemId, limit: CONVERSATION_INITIAL_VISIBLE_COUNT } - : { - ...(throughItemId ? { throughItemId } : {}), - limit: CONVERSATION_INITIAL_VISIBLE_COUNT, - }), - }, - ), + invoke('read_direct_project_history_slice', { + projectPath: nextProjectPath, + ...(beforeItemId + ? { beforeItemId, limit: CONVERSATION_INITIAL_VISIBLE_COUNT } + : { + ...(throughItemId ? { throughItemId } : {}), + limit: CONVERSATION_INITIAL_VISIBLE_COUNT, + }), + }), }); if (projectPathRef.current !== nextProjectPath) return; mergeHistoryPages(pages); @@ -751,15 +748,12 @@ export function useDirectProjectChatController({ existingEntries: directEntries, beforeItemId: historyOldestItemIdRef.current, readSlice: (beforeItemId) => - invoke( - 'read_direct_project_history_slice', - { - projectPath: nextProjectPath, - ...(beforeItemId - ? { beforeItemId, limit: DIRECT_HISTORY_PAGE_SIZE } - : { limit: DIRECT_HISTORY_PAGE_SIZE }), - }, - ), + invoke('read_direct_project_history_slice', { + projectPath: nextProjectPath, + ...(beforeItemId + ? { beforeItemId, limit: DIRECT_HISTORY_PAGE_SIZE } + : { limit: DIRECT_HISTORY_PAGE_SIZE }), + }), }); if (projectPathRef.current !== nextProjectPath) return; directThread.mergeHistoryItems(pages.items); diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectThreadChatSubscription.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectThreadChatSubscription.ts index 75d2d9ebe..0f6d6dcee 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectThreadChatSubscription.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectThreadChatSubscription.ts @@ -20,9 +20,9 @@ import { resolveDirectThreadBootstrap, selectDirectChatEntries, } from '../conversation/directThreadChat'; -import type { DirectThreadConsumeResult } from '../generated/DirectThreadConsumeResult'; -import type { DirectThreadItem } from '../generated/DirectThreadItem'; -import type { DirectThreadSubscriptionBootstrap } from '../generated/DirectThreadSubscriptionBootstrap'; +import type { ConsumeResult } from '../generated/ConsumeResult'; +import type { SubscriptionBootstrap } from '../generated/SubscriptionBootstrap'; +import type { ThreadItem } from '../generated/ThreadItem'; import { type DirectHistoryAnchorGate, reuseOrOpenDirectHistoryAnchorGate, @@ -43,7 +43,7 @@ export type DirectThreadChatSubscription = { /** 订阅回执锚点闸门:首屏历史读取靠它拿到 `lastCompletedItemId`。 */ anchorGateRef: MutableRefObject; /** 历史切片并入同一个 reducer:条目只有这一份事实源。 */ - mergeHistoryItems: (items: readonly DirectThreadItem[]) => void; + mergeHistoryItems: (items: readonly ThreadItem[]) => void; }; /** @@ -111,7 +111,7 @@ export function useDirectThreadChatSubscription({ try { do { consumeAgain = false; - const result = await invoke( + const result = await invoke( 'consume_direct_project_thread', { subscriptionId }, ); @@ -140,7 +140,7 @@ export function useDirectThreadChatSubscription({ } } async function bootstrap() { - const result = await invoke( + const result = await invoke( 'subscribe_direct_project_thread', { projectPath }, ); @@ -189,7 +189,7 @@ export function useDirectThreadChatSubscription({ }, [enabled, projectPath]); const mergeHistoryItems = useMemo( - () => (items: readonly DirectThreadItem[]) => { + () => (items: readonly ThreadItem[]) => { setState((current) => mergeDirectHistoryItems(current, items)); }, [], diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/conversation/directThreadChat.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/conversation/directThreadChat.ts index 12a1d728b..1e799e3d3 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/conversation/directThreadChat.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/conversation/directThreadChat.ts @@ -13,11 +13,11 @@ */ import type { GameCreatorDirectToolCall } from '../../../../app/types'; -import type { DirectThreadConsumeResult } from '../generated/DirectThreadConsumeResult'; -import type { DirectThreadEvent } from '../generated/DirectThreadEvent'; -import type { DirectThreadHistorySlice } from '../generated/DirectThreadHistorySlice'; -import type { DirectThreadItem } from '../generated/DirectThreadItem'; -import type { DirectThreadSubscriptionBootstrap } from '../generated/DirectThreadSubscriptionBootstrap'; +import type { ConsumeResult } from '../generated/ConsumeResult'; +import type { HistorySlice } from '../generated/HistorySlice'; +import type { SubscriptionBootstrap } from '../generated/SubscriptionBootstrap'; +import type { ThreadEvent } from '../generated/ThreadEvent'; +import type { ThreadItem } from '../generated/ThreadItem'; import { type DirectPendingTurn, enqueuePendingTurn, @@ -143,7 +143,7 @@ function validBoundaryAt(value: number | null | undefined): number { * 它与条目里的 `item.at` 是两件事:后者是条目展示时间,不作工具计时的起止边界。 * 字段由生成绑定声明(ts-rs),重放沿用原值,因此这里只读不取当前时间。 */ -export function readDirectThreadEventAt(event: DirectThreadEvent): number { +export function readDirectThreadEventAt(event: ThreadEvent): number { return validBoundaryAt('at' in event ? event.at : 0); } @@ -153,9 +153,7 @@ export function readDirectThreadEventAt(event: DirectThreadEvent): number { * 字段可选:缺失表示身份不可证明(旧事件保持原有顺序语义),此时不猜历史归属,也不拿 * 时间戳近似。 */ -export function readDirectThreadEventUserItemId( - event: DirectThreadEvent, -): string { +export function readDirectThreadEventUserItemId(event: ThreadEvent): string { const value = 'userItemId' in event ? event.userItemId : ''; return typeof value === 'string' ? value.trim() : ''; } @@ -318,7 +316,7 @@ function stampTurnUserEntry( function appendLiveText( state: DirectThreadChatState, - event: Extract, + event: Extract, ): DirectThreadChatState { const itemId = event.itemId.trim(); if (!itemId || !event.delta) return state; @@ -334,7 +332,7 @@ function appendLiveText( export function reduceDirectThreadEvent( state: DirectThreadChatState, - event: DirectThreadEvent, + event: ThreadEvent, ): DirectThreadChatState { switch (event.type) { case 'turn.started': { @@ -532,7 +530,7 @@ export function directThreadTurnMatchesUser( export function reduceDirectThreadEvents( state: DirectThreadChatState, - events: readonly DirectThreadEvent[], + events: readonly ThreadEvent[], ): DirectThreadChatState { return events.reduce(reduceDirectThreadEvent, state); } @@ -545,7 +543,7 @@ export function reduceDirectThreadEvents( */ export function resolveDirectThreadBootstrap( state: DirectThreadChatState, - bootstrap: DirectThreadSubscriptionBootstrap, + bootstrap: SubscriptionBootstrap, ): DirectThreadChatState { return reduceDirectThreadEvents(state, bootstrap.events); } @@ -553,7 +551,7 @@ export function resolveDirectThreadBootstrap( /** 事件顺序 = 游标顺序;调用方只需要把 `consume` 的结果喂进来。 */ export function applyDirectThreadConsumeResult( state: DirectThreadChatState, - result: DirectThreadConsumeResult, + result: ConsumeResult, ): DirectThreadChatState { return reduceDirectThreadEvents(state, result.events); } @@ -580,7 +578,7 @@ export function mergeHistoryEntries( /** 历史切片条目 → 聊天条目:可见性判定的唯一入口,分页判据也读这一份。 */ export function projectDirectHistoryItems( - items: readonly DirectThreadItem[], + items: readonly ThreadItem[], ): DirectChatEntry[] { return items .map((item) => projectDirectThreadItem(item)) @@ -594,7 +592,7 @@ export function projectDirectHistoryItems( */ export function mergeDirectHistoryItems( state: DirectThreadChatState, - items: readonly DirectThreadItem[], + items: readonly ThreadItem[], ): DirectThreadChatState { const history = mergeHistoryEntries( projectDirectHistoryItems(items), @@ -619,7 +617,7 @@ export function mergeDirectHistoryItems( export function mergeDirectThreadHistorySlice( state: DirectThreadChatState, - slice: DirectThreadHistorySlice, + slice: HistorySlice, ): DirectThreadChatState { return mergeDirectHistoryItems(state, slice.items); } diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/conversation/directThreadItemProjection.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/conversation/directThreadItemProjection.ts index 8d2bec2e3..2e09173fb 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/conversation/directThreadItemProjection.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/conversation/directThreadItemProjection.ts @@ -1,7 +1,7 @@ /** * DirectProject「原始条目 → 聊天条目」投影。 * - * 输入是 Rust 侧 ts-rs 导出的 `DirectThreadItem`(脱敏后的 Codex 原始条目),工具卡片的 + * 输入是 Rust 侧 ts-rs 导出的 `ThreadItem`(脱敏后的 Codex 原始条目),工具卡片的 * `kind`、标题、折叠摘要、状态判定和可见性全部在这里完成。运行态事件与历史切片走同一个 * 函数,因此实时与回读不可能出现两套口径。 */ @@ -12,7 +12,7 @@ import type { GameCreatorDirectToolCallKind, GameCreatorDirectToolCallStatus, } from '../../../../app/types'; -import type { DirectThreadItem } from '../generated/DirectThreadItem'; +import type { ThreadItem } from '../generated/ThreadItem'; import type { DirectChatEntry, DirectChatToolCard } from './directThreadChat'; /** 折叠态摘要上限,与卡片契约一致。 */ @@ -90,7 +90,7 @@ function toolTitle( } function fileChanges( - item: Extract, + item: Extract, ): GameCreatorDirectToolCallChange[] { return item.changes .filter((change) => change.path.trim().length > 0) @@ -201,7 +201,7 @@ function buildToolCard( } function toolCardFromItem( - item: DirectThreadItem, + item: ThreadItem, phase: DirectItemPhaseTime, ): DirectChatToolCard | null { switch (item.itemType) { @@ -293,7 +293,7 @@ function toolCardFromItem( * 聊天视图。`other` 是 Rust 原样透传的未知类型,要不要显示属于前端可见性决策,当前不显示。 */ export function projectDirectThreadItem( - item: DirectThreadItem | null | undefined, + item: ThreadItem | null | undefined, phase: DirectItemPhaseTime = {}, ): DirectChatEntry | null { if (!item) return null; diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/ConsumeResult.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/ConsumeResult.ts new file mode 100644 index 000000000..f31d8f49c --- /dev/null +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/ConsumeResult.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadEvent } from './ThreadEvent'; + +export type ConsumeResult = { events: Array }; diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadConsumeResult.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadConsumeResult.ts deleted file mode 100644 index ea7e6e112..000000000 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadConsumeResult.ts +++ /dev/null @@ -1,4 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DirectThreadEvent } from './DirectThreadEvent'; - -export type DirectThreadConsumeResult = { events: Array }; diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadHistorySlice.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/HistorySlice.ts similarity index 73% rename from apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadHistorySlice.ts rename to apps/ai-game-creator-shell/src/view/project-development/chat/generated/HistorySlice.ts index 9ac30f139..22a93077c 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadHistorySlice.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/HistorySlice.ts @@ -1,11 +1,11 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DirectThreadItem } from './DirectThreadItem'; +import type { ThreadItem } from './ThreadItem'; -export type DirectThreadHistorySlice = { +export type HistorySlice = { /** * 脱敏条目,顺序即文件顺序;与运行态事件里的条目同形。 */ - items: Array; + items: Array; hasMore: boolean; /** * 本次切片的原始 item id 锚点:无论切片里有没有可显示条目,分页都靠它向前。 diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectQueueRemovalOutcome.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/QueueRemovalOutcome.ts similarity index 62% rename from apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectQueueRemovalOutcome.ts rename to apps/ai-game-creator-shell/src/view/project-development/chat/generated/QueueRemovalOutcome.ts index 5a70720f9..109b6f6ba 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectQueueRemovalOutcome.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/QueueRemovalOutcome.ts @@ -3,7 +3,4 @@ /** * 取消一条待发消息的结果。 */ -export type DirectQueueRemovalOutcome = - | 'removed' - | 'alreadyDispatched' - | 'notFound'; +export type QueueRemovalOutcome = 'removed' | 'alreadyDispatched' | 'notFound'; diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectQueueRemovalReason.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/QueueRemovalReason.ts similarity index 85% rename from apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectQueueRemovalReason.ts rename to apps/ai-game-creator-shell/src/view/project-development/chat/generated/QueueRemovalReason.ts index c6c619049..f965308e6 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectQueueRemovalReason.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/QueueRemovalReason.ts @@ -6,4 +6,4 @@ * typed 枚举,取值即语义:取消是用户在输入盒上撤掉这条消息,放行是它已经离开队列并成为回合 * (同一临界区里另有 `turn.started`)。界面按它分流,不解析字符串。 */ -export type DirectQueueRemovalReason = 'cancelled' | 'dispatched'; +export type QueueRemovalReason = 'cancelled' | 'dispatched'; diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadSubscriptionBootstrap.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/SubscriptionBootstrap.ts similarity index 71% rename from apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadSubscriptionBootstrap.ts rename to apps/ai-game-creator-shell/src/view/project-development/chat/generated/SubscriptionBootstrap.ts index 45efbf4b4..de5eb7404 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadSubscriptionBootstrap.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/SubscriptionBootstrap.ts @@ -1,7 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DirectThreadEvent } from './DirectThreadEvent'; +import type { ThreadEvent } from './ThreadEvent'; -export type DirectThreadSubscriptionBootstrap = { +export type SubscriptionBootstrap = { subscriptionId: string; /** * 首屏历史锚点:`project.jsonl` 里最后一条原始 item id。 @@ -10,5 +10,5 @@ export type DirectThreadSubscriptionBootstrap = { /** * 该 subscriber 此刻应当处理的运行态事件(游标已经在队尾)。 */ - events: Array; + events: Array; }; diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadDeltaKind.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/ThreadDeltaKind.ts similarity index 71% rename from apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadDeltaKind.ts rename to apps/ai-game-creator-shell/src/view/project-development/chat/generated/ThreadDeltaKind.ts index 215819e1a..b414a0f81 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadDeltaKind.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/ThreadDeltaKind.ts @@ -3,4 +3,4 @@ /** * 增量正文属于哪类条目。 */ -export type DirectThreadDeltaKind = 'message' | 'reasoning'; +export type ThreadDeltaKind = 'message' | 'reasoning'; diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadEvent.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/ThreadEvent.ts similarity index 85% rename from apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadEvent.ts rename to apps/ai-game-creator-shell/src/view/project-development/chat/generated/ThreadEvent.ts index 55c0b6a94..184fe739b 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadEvent.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/ThreadEvent.ts @@ -1,10 +1,10 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { DirectCodexUserItem } from './DirectCodexUserItem'; -import type { DirectQueueRemovalReason } from './DirectQueueRemovalReason'; -import type { DirectThreadDeltaKind } from './DirectThreadDeltaKind'; -import type { DirectThreadItem } from './DirectThreadItem'; -import type { DirectThreadRequestKind } from './DirectThreadRequestKind'; import type { DirectTurnFailure } from './DirectTurnFailure'; +import type { QueueRemovalReason } from './QueueRemovalReason'; +import type { ThreadDeltaKind } from './ThreadDeltaKind'; +import type { ThreadItem } from './ThreadItem'; +import type { ThreadRequestKind } from './ThreadRequestKind'; /** * Thread Manager 下发的运行态事件。 @@ -20,7 +20,7 @@ import type { DirectTurnFailure } from './DirectTurnFailure'; * `item.at` 只说明"这条条目什么时候被看到",工具计时不得拿它当开始或完成边界。 * 条目阶段优先用通知层的毫秒字段(`startedAtMs` / `completedAtMs`),缺失才用宿主钟; * 回合阶段没有可用的毫秒上游字段(Turn 只有秒级 `startedAt` / `completedAt`),一律用宿主 - * 在该阶段取的毫秒钟——见 `direct_thread_turn_completed_at_ms` 的说明。 + * 在该阶段取的毫秒钟——见 `thread_turn_completed_at_ms` 的说明。 * `at` 在事件进入 Thread Manager 时就固定:重放(bootstrap / consume)必须沿用原值, * 不能在前端收到或重放时重新取当前时间。 * @@ -31,7 +31,7 @@ import type { DirectTurnFailure } from './DirectTurnFailure'; * + 历史切片、运行态一直为空时也能按身份认领开口条目,不必靠时间戳猜。缺失表示身份不可证明 * (旧事件、没有开口用户条目、取消时拿不到 clientTurnId),此时前端不得补造。 */ -export type DirectThreadEvent = +export type ThreadEvent = | { type: 'turn.started'; /** @@ -66,7 +66,7 @@ export type DirectThreadEvent = } | { type: 'item.started'; - item: DirectThreadItem; + item: ThreadItem; /** * 条目开始执行的原生阶段时间(毫秒);缺失时是宿主观测到该阶段的时间。 */ @@ -74,19 +74,14 @@ export type DirectThreadEvent = } | { type: 'item.completed'; - item: DirectThreadItem; + item: ThreadItem; /** * 条目结束的原生阶段时间(毫秒);缺失时是宿主观测到该阶段的时间。 */ at?: number; } - | { - type: 'item.delta'; - itemId: string; - kind: DirectThreadDeltaKind; - delta: string; - } - | { type: 'request'; kind: DirectThreadRequestKind; requestId: string | null } + | { type: 'item.delta'; itemId: string; kind: ThreadDeltaKind; delta: string } + | { type: 'request'; kind: ThreadRequestKind; requestId: string | null } | { type: 'queue.enqueued'; /** @@ -106,6 +101,6 @@ export type DirectThreadEvent = | { type: 'queue.removed'; clientTurnId: string; - reason: DirectQueueRemovalReason; + reason: QueueRemovalReason; at?: number; }; diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadFileChange.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/ThreadFileChange.ts similarity index 85% rename from apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadFileChange.ts rename to apps/ai-game-creator-shell/src/view/project-development/chat/generated/ThreadFileChange.ts index a510ccb68..5ef619872 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadFileChange.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/ThreadFileChange.ts @@ -3,7 +3,7 @@ /** * 一条文件变更。 */ -export type DirectThreadFileChange = { +export type ThreadFileChange = { path: string; /** * `add` | `update` | `delete` diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadItem.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/ThreadItem.ts similarity index 89% rename from apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadItem.ts rename to apps/ai-game-creator-shell/src/view/project-development/chat/generated/ThreadItem.ts index f1257ddcc..cbb236f88 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadItem.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/ThreadItem.ts @@ -1,16 +1,16 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DirectThreadFileChange } from './DirectThreadFileChange'; +import type { ThreadFileChange } from './ThreadFileChange'; /** * 聊天视图的输入条目:一条 Codex 原始条目的脱敏投影。 * * `itemType` 就是 Codex 的原始类型,逐字透传;前端按它决定投影成消息、思考还是工具卡片。 - * 未识别的类型走 [`DirectThreadItem::Other`],Rust 不替前端决定它是否可见。 + * 未识别的类型走 [`ThreadItem::Other`],Rust 不替前端决定它是否可见。 * * 条目上的 `at` 是只用于显示的毫秒时间戳:ts-rs 默认把 `u64` 映射成 `bigint`, * 而 Tauri 的 JSON 通道传过来的是 `number`,因此统一标 `#[ts(as = "f64")]` 对齐。 */ -export type DirectThreadItem = +export type ThreadItem = | { itemType: 'message'; /** @@ -53,7 +53,7 @@ export type DirectThreadItem = | { itemType: 'fileChange'; itemId: string; - changes: Array; + changes: Array; at: number; } | { diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadRequestKind.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/ThreadRequestKind.ts similarity index 87% rename from apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadRequestKind.ts rename to apps/ai-game-creator-shell/src/view/project-development/chat/generated/ThreadRequestKind.ts index 13ee401f3..1b946a72b 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectThreadRequestKind.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/ThreadRequestKind.ts @@ -3,7 +3,7 @@ /** * 审批 / 提问请求与解决:本轮只透传,不并入聊天状态。 */ -export type DirectThreadRequestKind = +export type ThreadRequestKind = | 'approval.requested' | 'ask.requested' | 'request.resolved'; diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/history/directHistoryPaging.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/history/directHistoryPaging.ts index ead30799d..f562ae6f6 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/history/directHistoryPaging.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/history/directHistoryPaging.ts @@ -15,12 +15,12 @@ import { projectDirectHistoryItems, } from '../conversation/directThreadChat'; import { buildDirectChatTurns } from '../conversation/directTurnPresentation'; -import type { DirectThreadHistorySlice } from '../generated/DirectThreadHistorySlice'; -import type { DirectThreadItem } from '../generated/DirectThreadItem'; +import type { HistorySlice } from '../generated/HistorySlice'; +import type { ThreadItem } from '../generated/ThreadItem'; export type DirectHistoryPages = { /** 累积到的条目,保持文件顺序(旧 → 新)。 */ - items: DirectThreadItem[]; + items: ThreadItem[]; /** 后端声明的"还有更早的历史";取不动时收口为 false。 */ hasMore: boolean; /** 下一屏的锚点;取不动或失败时停在最后一个可用的锚点上。 */ @@ -34,7 +34,7 @@ function visibleTurnCount(entries: readonly DirectChatEntry[]): number { } /** 取页是"从新往旧"的,拼回文件顺序要整个翻过来。 */ -function inFileOrder(pages: readonly (readonly DirectThreadItem[])[]) { +function inFileOrder(pages: readonly (readonly ThreadItem[])[]) { return [...pages].reverse().flat(); } @@ -46,15 +46,15 @@ export async function readDirectHistoryPages({ /** 当前聊天视图里的条目:判据必须和用户看到的是同一份。 */ existingEntries: readonly DirectChatEntry[]; beforeItemId: string | null; - readSlice: (beforeItemId: string | null) => Promise; + readSlice: (beforeItemId: string | null) => Promise; }): Promise { const baseTurnCount = visibleTurnCount(existingEntries); - const pages: DirectThreadItem[][] = []; + const pages: ThreadItem[][] = []; let anchor = beforeItemId; let hasMore = false; let error: unknown = null; for (let page = 0; page < DIRECT_HISTORY_MAX_PAGES_PER_ACTION; page += 1) { - let slice: DirectThreadHistorySlice; + let slice: HistorySlice; try { slice = await readSlice(anchor); } catch (thrown) { diff --git a/apps/ai-game-creator-shell/tests/directHistoryPaging.test.ts b/apps/ai-game-creator-shell/tests/directHistoryPaging.test.ts index 02f3c1240..3e620c89f 100644 --- a/apps/ai-game-creator-shell/tests/directHistoryPaging.test.ts +++ b/apps/ai-game-creator-shell/tests/directHistoryPaging.test.ts @@ -7,15 +7,15 @@ import { mergeDirectHistoryItems, selectDirectChatEntries, } from '../src/view/project-development/chat/conversation/directThreadChat'; -import type { DirectThreadHistorySlice } from '../src/view/project-development/chat/generated/DirectThreadHistorySlice'; -import type { DirectThreadItem } from '../src/view/project-development/chat/generated/DirectThreadItem'; +import type { HistorySlice } from '../src/view/project-development/chat/generated/HistorySlice'; +import type { ThreadItem } from '../src/view/project-development/chat/generated/ThreadItem'; import { readDirectHistoryPages } from '../src/view/project-development/chat/history/directHistoryPaging'; -function userItem(itemId: string): DirectThreadItem { +function userItem(itemId: string): ThreadItem { return { itemType: 'message', itemId, role: 'user', text: itemId, at: 1000 }; } -function assistantItem(itemId: string): DirectThreadItem { +function assistantItem(itemId: string): ThreadItem { return { itemType: 'message', itemId, @@ -25,7 +25,7 @@ function assistantItem(itemId: string): DirectThreadItem { }; } -function toolItem(itemId: string): DirectThreadItem { +function toolItem(itemId: string): ThreadItem { return { itemType: 'function_call', itemId, @@ -35,26 +35,26 @@ function toolItem(itemId: string): DirectThreadItem { }; } -function reasoningItem(itemId: string): DirectThreadItem { +function reasoningItem(itemId: string): ThreadItem { return { itemType: 'reasoning', itemId, text: '想想', at: 1400 }; } function slice( - items: DirectThreadItem[], + items: ThreadItem[], hasMore: boolean, firstItemId: string | null, -): DirectThreadHistorySlice { +): HistorySlice { return { items, hasMore, firstItemId }; } /** 用生产投影把条目拉成聊天条目:判据必须和视图看到的是同一份。 */ -function entriesOf(items: DirectThreadItem[]): DirectChatEntry[] { +function entriesOf(items: ThreadItem[]): DirectChatEntry[] { return selectDirectChatEntries( mergeDirectHistoryItems(emptyDirectThreadChatState(), items), ); } -function readerOf(pages: DirectThreadHistorySlice[]) { +function readerOf(pages: HistorySlice[]) { const remaining = [...pages]; return vi.fn(async (_beforeItemId: string | null) => { const next = remaining.shift(); diff --git a/apps/ai-game-creator-shell/tests/directThreadChat.test.ts b/apps/ai-game-creator-shell/tests/directThreadChat.test.ts index bf252baff..c5a7bffc9 100644 --- a/apps/ai-game-creator-shell/tests/directThreadChat.test.ts +++ b/apps/ai-game-creator-shell/tests/directThreadChat.test.ts @@ -12,29 +12,27 @@ import { resolveDirectThreadBootstrap, selectDirectChatEntries, } from '../src/view/project-development/chat/conversation/directThreadChat'; -import type { DirectThreadItem } from '../src/view/project-development/chat/conversation/directThreadItemProjection'; -import type { DirectThreadEvent } from '../src/view/project-development/chat/generated/DirectThreadEvent'; +import type { ThreadItem } from '../src/view/project-development/chat/conversation/directThreadItemProjection'; +import type { ThreadEvent } from '../src/view/project-development/chat/generated/ThreadEvent'; function event( - partial: Pick & Partial, -): DirectThreadEvent { - return partial as DirectThreadEvent; + partial: Pick & Partial, +): ThreadEvent { + return partial as ThreadEvent; } /** 生命周期事件上的 canonical user identity(原生 `userItemId`)。 */ function withUserItemId( - lifecycle: DirectThreadEvent, + lifecycle: ThreadEvent, userItemId: string, -): DirectThreadEvent { +): ThreadEvent { return { ...lifecycle, userItemId }; } /** app-server `item/started`:工具真正开始执行,itemId 就是归一后的唯一身份。 */ function toolStarted( - overrides: Partial< - Extract - > = {}, -): DirectThreadItem { + overrides: Partial> = {}, +): ThreadItem { return { itemType: 'function_call', itemId: 'call_00_Gpd0s0Ytm9YgIbwbEXva1473', @@ -46,7 +44,7 @@ function toolStarted( } /** 原始 response item 的输出条目:Rust 已经把它归一成同一个 itemId。 */ -function toolOutput(): DirectThreadItem { +function toolOutput(): ThreadItem { return { itemType: 'function_call_output', itemId: 'call_00_Gpd0s0Ytm9YgIbwbEXva1473', @@ -56,8 +54,8 @@ function toolOutput(): DirectThreadItem { } function messageItem( - overrides: Partial> = {}, -): DirectThreadItem { + overrides: Partial> = {}, +): ThreadItem { return { itemType: 'message', itemId: 'msg-1', @@ -205,7 +203,7 @@ describe('DirectProject 聊天 reducer', () => { itemId: 'plan-1', rawType: 'plan', at: 0, - } satisfies DirectThreadItem, + } satisfies ThreadItem, }), ]); expect(selectDirectChatEntries(state)).toHaveLength(0); @@ -297,7 +295,7 @@ describe('DirectProject 聊天 reducer', () => { id: `direct-codex:${clientTurnId}:user`, content: [{ type: 'input_text', text }], }, - } as Partial); + } as Partial); } it('按事件顺序折成队列,取消与放行都只按身份移除', () => { @@ -582,7 +580,7 @@ describe('DirectProject 聊天 reducer', () => { status: 'failed', at: 2_000, failure: { kind: 'host-dropped', message }, - } as unknown as DirectThreadEvent; + } as unknown as ThreadEvent; const failed = reduceDirectThreadEvents(emptyDirectThreadChatState(), [ withUserItemId( event({ type: 'turn.started', at: 1_000 }),