From 7e610c52bcff599593a261b4ec65697918d6dccc Mon Sep 17 00:00:00 2001 From: limityan Date: Sun, 19 Jul 2026 00:17:05 +0800 Subject: [PATCH] refactor(acp): route session mode updates through runtime Add a narrow typed session mode port, wire it to the existing Core owner, and migrate ACP mode changes without changing history replay or other product surfaces. --- .../agent-runtime-services-design.md | 23 +-- docs/architecture/cli-product-line-design.md | 2 +- docs/architecture/product-architecture.md | 2 +- docs/plans/core-decomposition-plan.md | 6 +- .../product-architecture-evolution-plan.md | 2 +- .../src/agentic/coordination/coordinator.rs | 83 +++++++++++ .../core/src/service_agent_runtime.rs | 17 ++- src/crates/contracts/runtime-ports/AGENTS.md | 3 + src/crates/contracts/runtime-ports/src/lib.rs | 19 +++ .../execution/agent-runtime/src/runtime.rs | 135 +++++++++++++++--- src/crates/execution/agent-runtime/src/sdk.rs | 37 +++-- src/crates/interfaces/acp/AGENTS-CN.md | 4 +- src/crates/interfaces/acp/AGENTS.md | 12 +- .../interfaces/acp/src/runtime/session.rs | 19 ++- 14 files changed, 301 insertions(+), 63 deletions(-) diff --git a/docs/architecture/agent-runtime-services-design.md b/docs/architecture/agent-runtime-services-design.md index 25c5fc473..9c6a115fa 100644 --- a/docs/architecture/agent-runtime-services-design.md +++ b/docs/architecture/agent-runtime-services-design.md @@ -13,8 +13,8 @@ CLI Agent 体验边界见 [`cli-product-line-design.md`](cli-product-line-design 新增公共类型前必须有真实生产调用方、版本边界和验证路径。现有 Agent Runtime SDK 仍是 v1 preview,CLI、ACP、 Desktop 仍保留 `bitfun-core/product-full` 兼容 owner。CLI 与 CLI 托管的 ACP server 已消费各自的产品组装结果; Desktop 主交互只消费由现有 Core owner 构造的窄口径 SDK 门面,尚未组装完整 Desktop profile。这些接入都不等于 -协调器、调度器、持久化或工具执行 owner 已迁移;ACP 的完整持久化历史、模式、模型目录/提供方配置、MCP、客户端路径与 -Desktop 的其余入口仍保留明确的兼容边界,活动会话的模型写入已通过 SDK 回到 Core owner。 +协调器、调度器、持久化或工具执行 owner 已迁移;ACP 的完整持久化历史、模型/模式目录与提供方配置、MCP、客户端路径与 +Desktop 的其余入口仍保留明确的兼容边界,活动会话的模型/模式写入已通过 SDK 回到 Core owner。 阅读路径:第 1 节确认 SDK、内核、产品特性、扩展接口和 crate 边界;第 2-3 节说明稳定接口、 运行时服务、内核、工具和工作流;第 4 节说明产品组装与扩展注册;第 5 节作为质量保护和 @@ -720,7 +720,7 @@ Core 的 Network、Git 和 MCP Catalog 当前仍含兼容 marker,因此该诊 该切换仍是 `product-full` 兼容组装,不是 owner 迁移。协调器、调度器、持久化、工具管线和 Agentic Event Queue 仍由 Core 唯一持有;CLI 与 ACP 不复制这些状态。ACP 服务端通过 SDK 处理会话创建/列举、轮次、取消、交互响应和事件订阅, -但完整持久化历史回放、模式、模型目录/配置和 MCP 仍走单一 Core 兼容门面;会话模型写入通过 SDK 回到同一 Core owner。ACP stdio、连接和协议投影仍在 +但完整持久化历史回放、模型/模式目录与提供方配置和 MCP 仍走单一 Core 兼容门面;会话模型/模式写入通过 SDK 回到同一 Core owner。ACP stdio、连接和协议投影仍在 `interfaces/acp`。Desktop 复用同一 Core owner 构造一个窄口径 SDK 门面,主界面的轮次提交/取消、工具确认/拒绝和 用户问题回答与会话模型更新已通过该门面;会话 CRUD/恢复视图、MCP、MiniApp、Cron、远程连接、Tauri 窗口与平台资源 仍保留在 Desktop/Core 兼容入口。Server 仅提供健康检查、信息与 ping 路由。未接入入口的 profile、枚举分支和 @@ -796,13 +796,14 @@ Core 的 Network、Git 和 MCP Catalog 当前仍含兼容 marker,因此该诊 | ACP | ACP 协议、客户端生命周期、远端探测 | 外部智能体/工具能力、环境事实、权限桥接 | | Web UI / mobile web | UI 状态、hydration、配对、会话展示、插件状态视图 | 接口/传输 DTO、运行时事件事实、能力服务读模型 | -当前 Runtime SDK 已提供会话创建、列出、删除、恢复、模型更新、类型化转录读取、本地分支、用量生成,以及 -精确轮次结算。模型更新只接受会话 ID 与模型 ID, -不承载模型目录、模式、提供方配置或宿主 UI 选择语义。`AgentSessionRestoreRequest/Result` 与 +当前 Runtime SDK 已提供会话创建、列出、删除、恢复、模型/模式更新、类型化转录读取、本地分支、用量生成,以及 +精确轮次结算。模型与模式更新只接受会话 ID 和被选择的稳定 ID, +不承载目录、提供方配置、选择策略或宿主 UI 语义。`AgentSessionRestoreRequest/Result` 与 `AgentSessionRestorePort` 归 Agent Runtime SDK,以继续复用 Runtime owner 的完整 `SessionState`;类型化 `SessionTranscript` 归 `runtime-ports`。两者都由 `assembly/core` 注入真实持久化 owner,当前由 CLI/TUI -消费;ACP 为保证模型配置与完整历史来自同一次恢复,继续通过 Core 兼容门面读取协议回放所需的完整轮次,避免为单一协议 -扩张通用 transcript。会话分支请求显式携带可选远程身份;当前本地 provider 对远程身份返回 `NotAvailable`,不据本地路径 +消费;ACP 的活动会话模式更新也通过窄端口回到同一 Core owner。ACP 为保证模型配置与完整历史来自同一次恢复,继续通过 +Core 兼容门面读取协议回放所需的完整轮次,避免为单一协议扩张通用 transcript,也不绕过附件内容所需的独立授权能力。 +会话分支请求显式携带可选远程身份;当前本地 provider 对远程身份返回 `NotAvailable`,不据本地路径 推断远程语义。CLI/TUI 的工具确认、拒绝和用户问题回答,以及 ACP 服务端 / Peer Host 的工具确认与拒绝,通过类型化 `AgentInteractionResponsePort` 回到 Core 的工具管线或用户输入 owner,不改变审批策略或交互所有权。 `CoreAgentRuntimeCompatibility` 仍承载未迁移的 @@ -871,8 +872,8 @@ Provider 装配同样按需增加,不提前为 Memory、Context、Workflow、S CLI 托管的 ACP 服务端使用 `DeliveryProfile::Acp` 组装一个 Agent Runtime,通过 SDK 处理会话创建/列举、轮次提交/取消、 交互响应和只读 Agent 事件订阅。ACP 只把共享运行时事实映射成协议更新;标准输入输出、连接、权限 RPC 与通知生命周期 -不进入 SDK。完整持久化历史恢复、模式、模型目录/提供方配置、MCP 和 ACP 客户端路径仍是明确的 Core 兼容范围; -活动会话的模型写入已经通过 SDK 回到 Core owner, +不进入 SDK。完整持久化历史恢复、模型/模式目录与提供方配置读取、MCP 和 ACP 客户端路径仍是明确的 Core 兼容范围; +活动会话的模型/模式写入已经通过 SDK 回到 Core owner, 不据此扩张通用 runtime DTO。 继续拥有: @@ -1086,7 +1087,7 @@ Product 测试: - 把 embedded relay 的 TCP bind、静态 fallback 和任务生命周期移出 assembly;room/device 状态、account/sync 存储、asset store 与 HTTP/WebSocket router 已归属 `services/relay-service`,Cargo 反向边已删除并由通用边界检查保护。 - 继续缩小 CLI 的 Core 兼容门面;只有稳定端口、真实生产调用方和行为等价测试齐备时才迁移 owner。 -- 继续按真实复用需求缩小 ACP 的完整持久化历史、模式、模型目录/提供方配置、MCP 与客户端兼容路径;Desktop 仅继续迁移存在稳定端口和 +- 继续按真实复用需求缩小 ACP 的完整持久化历史、模型/模式目录与提供方配置读取、MCP 与客户端兼容路径;Desktop 仅继续迁移存在稳定端口和 行为等价测试的入口。完整 Desktop 产品组装需先补齐真实必需服务与事件消费路径,不以桩实现提前声明能力;ACP 生命周期 和 Desktop 平台资源仍留在各自入口。 - 为 Agent Runtime SDK 增加至少一个非 `bitfun-core` 的真实嵌入方;预览 facade 和单元测试不等于外部可用 SDK。 diff --git a/docs/architecture/cli-product-line-design.md b/docs/architecture/cli-product-line-design.md index e58de1329..b08af076c 100644 --- a/docs/architecture/cli-product-line-design.md +++ b/docs/architecture/cli-product-line-design.md @@ -97,7 +97,7 @@ BitFun CLI 应成为可独立安装和发布的 Agent 产品,而不是 Desktop `exec --session-id` 和缺失后端会话通过独立固定 ID 方法按原 ID 重建)/列举/删除/恢复、类型化转录、本地分支、 用量生成、轮次提交/取消和精确结算;普通创建 DTO 保持 v1 字段集合,固定 ID 冲突返回 `InvalidRequest`。会话模型更新、工具确认/拒绝和用户问题回答也通过 - SDK 的窄端口回到 Core owner;模型目录、模式和提供方配置仍由产品入口解释。TUI 用量卡片持久化、快照及 Peer Host/ACP 维护 + SDK 的窄端口回到 Core owner,ACP 的活动会话模式更新也复用同一窄端口;模型/模式目录和提供方配置仍由产品入口解释。TUI 用量卡片持久化、快照及 Peer Host/ACP 维护 等 SDK v1 缺口由一个 Core 兼容门面转发给原 owner。 - Agentic Event Queue 仍是唯一事件 owner;TUI、`exec` 与 Peer Host 使用独立广播订阅,不互相消费事件。 - 有界旧队列只承担兼容存储;达到容量时不得抑制广播。CLI 保持一个后台 drain,订阅方一旦报告 lag/closed, diff --git a/docs/architecture/product-architecture.md b/docs/architecture/product-architecture.md index 1c60ddade..2ba44f3d4 100644 --- a/docs/architecture/product-architecture.md +++ b/docs/architecture/product-architecture.md @@ -331,7 +331,7 @@ flowchart LR | HarmonyOS PC 原生 CLI/TUI | 未来平台目标,当前未实现 | 目标、问题和风险见平台规约;具体适配另立专题,HAP、手机 Remote App 与远端代执行均不替代 | | HarmonyOS PC GUI | 完整 HarmonyOS PC 支持的另一目标形态,当前未实现 | 与 CLI/TUI 共享稳定能力和 Runtime 语义,但独立设计宿主、界面与发布验证;Web、Remote 或现有 Tauri Desktop 均不能替代 | | HarmonyOS 手机 Remote App | `src/apps/mobile/harmonyos` 是 phone-only ArkTS 远程入口,不持有本地 Rust Agent Runtime | 保持当前能力并按移动端专题独立演进;本轮不提前设计移动 Runtime/TUI/GUI,也不能据此宣称 HarmonyOS PC 本地能力 | -| ACP | CLI 托管的服务端仍以 `bitfun-core/product-full` 作为兼容执行层 | 入口已选择 `DeliveryProfile::Acp` 并消费 Runtime Parts;组装层在入队前原子拒绝忙碌会话,不改变其他产品入口的排队行为;活动会话模型写入走 SDK,会话恢复、模式、模型目录/提供方配置、MCP、客户端与协议生命周期仍留在现有 Core/ACP 归属,不据此宣称完整解耦 | +| ACP | CLI 托管的服务端仍以 `bitfun-core/product-full` 作为兼容执行层 | 入口已选择 `DeliveryProfile::Acp` 并消费 Runtime Parts;组装层在入队前原子拒绝忙碌会话,不改变其他产品入口的排队行为;活动会话模型与模式写入走 SDK。会话恢复及完整历史、模型/模式目录与配置读取、MCP、客户端与协议生命周期仍留在现有 Core/ACP 归属,不据此宣称完整解耦 | | Server / Remote | 当前生产路由没有插件状态消费闭环;Remote 插件执行未实现 | 不在本地替远端项目发现、准备或执行插件;未接入时返回明确不支持 | | Web / Mobile Web | 依赖现有后端入口,不持有插件执行单元 | 对应 profile 当前为空计划或未接入生产,不能据枚举值宣称独立产品能力 | | SDK | 仅有 preview 门面、空 profile 计划和测试替身 | 不牵引 `product-full`、具体服务管理器或插件 host ABI;未满足独立嵌入验证前不宣称可发布 | diff --git a/docs/plans/core-decomposition-plan.md b/docs/plans/core-decomposition-plan.md index d429c49ca..2b3907cbf 100644 --- a/docs/plans/core-decomposition-plan.md +++ b/docs/plans/core-decomposition-plan.md @@ -61,7 +61,7 @@ CLI 是首个入口迁移对象,因为它已有独立产品诉求、显式设 当前纵向切片已经完成:入口只提交一次 `DeliveryProfile::Cli`,通过现有 `ProductAssembler` 获得计划、服务可用性、 Harness 和禁用的插件 binding;TUI、Exec、Session 与 Usage 共用一个 `CliRuntimeContext`。主会话客户端的创建(包括 `exec --session-id` 和缺失后端会话通过独立固定 ID 方法按原 ID 重建)/列举/删除/恢复、类型化转录、本地分支、用量生成、 -会话模型更新、轮次提交/取消和精确轮次结算均走 Agent Runtime SDK;TUI 用量卡片持久化、快照、Peer Host/ACP 的维护等 +会话模型更新、ACP 活动会话模式更新、轮次提交/取消和精确轮次结算均走 Agent Runtime SDK;TUI 用量卡片持久化、快照、Peer Host/ACP 的维护等 未迁移能力继续集中在一个 Core 兼容门面。Agentic Event Queue 仍是唯一 owner,各入口只建立独立广播订阅,有界兼容队列满载不再阻断广播。TUI 与 Exec 审批均为调用级策略,不写全局 配置;CLI 本地路径不获取具体 PersistenceManager。交互、执行和管理入口分别控制 Peer Host/MCP 生命周期,管理查询不启动 @@ -73,8 +73,8 @@ Peer Host 的 Runtime 接入和跨 Relay/Desktop/Web 的协议切换保持独立 下一步按独立纵向切片推进: -1. 以真实调用方和行为等价测试逐项缩小快照及 Peer Host/ACP 持久化维护兼容面;远程分支另行定义身份和存储语义,模型目录与配置仍保留在产品入口。 -2. 继续迁移 ACP 尚未接入 SDK 的持久化历史、模型目录/模式和 MCP 操作;ACP stdio 与协议投影生命周期保留在接口入口。 +1. 以真实调用方和行为等价测试逐项缩小快照及 Peer Host 持久化维护兼容面;远程分支另行定义身份和存储语义,模型目录与配置仍保留在产品入口。 +2. ACP 完整历史继续使用单次 Core 兼容恢复;在出现第二个真实消费者及经过授权的附件读取能力前,不为协议回放扩张通用 transcript。模型/模式目录与配置读取、MCP、ACP stdio 和协议投影生命周期继续留在各自现有 owner。 3. 继续按真实故障样例拆分 TUI 副作用边界;当前切片已覆盖本地确定性流式模型夹具驱动的 Linux PTY/Windows ConPTY Chat resize/取消、 `exec` Ctrl+C、本地模型 HTTP 403 授权拒绝、流中断后的重试失败、`stream-json` Patch 写入失败和终端恢复错误聚合, 不以大规模重写替代现有回归保护。 diff --git a/docs/plans/product-architecture-evolution-plan.md b/docs/plans/product-architecture-evolution-plan.md index 854cdc1cd..330999172 100644 --- a/docs/plans/product-architecture-evolution-plan.md +++ b/docs/plans/product-architecture-evolution-plan.md @@ -110,7 +110,7 @@ HarmonyOS 手机 Remote App 不在该平台执行范围内。 类型化转录、本地分支、用量生成、轮次提交/取消与精确结算、活动会话模型更新和 TUI 工具确认/拒绝/用户问题回答 已由真实入口消费 Runtime SDK;交互模式下的 Peer Host 也通过同一 SDK 提交/精确取消 turn、更新活动会话模型并处理工具 确认/拒绝。远程分支保持不支持;TUI 用量卡片持久化、快照和 Peer Host/ACP 维护等产品操作继续由现有单一兼容路径转发。 -- ACP:CLI 托管服务端的会话创建/列举、轮次提交/取消、活动会话模型更新、工具确认/拒绝、用户问题回答和事件订阅 +- ACP:CLI 托管服务端的会话创建/列举、轮次提交/取消、活动会话模型/模式更新、工具确认/拒绝、用户问题回答和事件订阅 已复用同一 SDK 语义;ACP stdio、连接、权限 RPC 与通知生命周期仍留在接口入口。 - Desktop:主界面轮次提交/取消、活动会话模型更新、工具确认/拒绝和用户问题回答已通过现有协调器与调度器端口构造的窄口径 SDK 门面; 完整产品组装需等待真实必需服务与事件消费路径。会话 CRUD/恢复视图、模型目录/提供方配置、MCP、MiniApp、Cron、远程连接、 diff --git a/src/crates/assembly/core/src/agentic/coordination/coordinator.rs b/src/crates/assembly/core/src/agentic/coordination/coordinator.rs index d3f4e39a8..f50f48606 100644 --- a/src/crates/assembly/core/src/agentic/coordination/coordinator.rs +++ b/src/crates/assembly/core/src/agentic/coordination/coordinator.rs @@ -8126,6 +8126,18 @@ impl bitfun_runtime_ports::AgentSessionModelPort for ConversationCoordinator { } } +#[async_trait::async_trait] +impl bitfun_runtime_ports::AgentSessionModePort for ConversationCoordinator { + async fn update_session_mode( + &self, + request: bitfun_runtime_ports::AgentSessionModeUpdateRequest, + ) -> bitfun_runtime_ports::PortResult<()> { + self.update_session_agent_type(&request.session_id, &request.mode_id) + .await + .map_err(runtime_port_error_preserving_message) + } +} + #[async_trait::async_trait] impl bitfun_agent_runtime::sdk::AgentSessionRestorePort for ConversationCoordinator { async fn restore_session( @@ -8648,6 +8660,77 @@ mod tests { assert!(error.message.contains("missing-session")); } + #[tokio::test] + async fn session_mode_port_preserves_core_not_found_errors() { + use bitfun_agent_runtime::sdk::{AgentSessionModePort, AgentSessionModeUpdateRequest}; + + let (coordinator, _) = test_coordinator(); + let error = AgentSessionModePort::update_session_mode( + &coordinator, + AgentSessionModeUpdateRequest { + session_id: "missing-session".to_string(), + mode_id: "agentic".to_string(), + }, + ) + .await + .expect_err("missing session must remain a typed not-found error"); + + assert_eq!(error.kind, bitfun_runtime_ports::PortErrorKind::NotFound); + assert!(error.message.contains("missing-session")); + } + + #[tokio::test] + async fn session_mode_runtime_updates_the_real_core_session() { + use bitfun_agent_runtime::sdk::{AgentRuntimeBuilder, AgentSessionModeUpdateRequest}; + + let (coordinator, session_manager) = test_coordinator(); + let coordinator = Arc::new(coordinator); + let workspace_path = std::env::temp_dir().join(format!( + "bitfun-session-mode-runtime-test-{}", + uuid::Uuid::new_v4() + )); + std::fs::create_dir_all(&workspace_path).expect("workspace dir should exist"); + let workspace_path_string = workspace_path.to_string_lossy().into_owned(); + let session = TEST_AGENT_MODEL_DEFAULTS + .scope( + AgentModelDefaultsConfig::default(), + coordinator.create_session_with_workspace( + None, + "Runtime mode update".to_string(), + "agentic".to_string(), + SessionConfig { + workspace_path: Some(workspace_path_string.clone()), + ..Default::default() + }, + workspace_path_string, + ), + ) + .await + .expect("real Core session should be created"); + let runtime = AgentRuntimeBuilder::new() + .with_submission_port(coordinator.clone()) + .with_session_mode_port(coordinator) + .build() + .expect("assembled agent runtime"); + + runtime + .update_session_mode(AgentSessionModeUpdateRequest { + session_id: session.session_id.clone(), + mode_id: " plan ".to_string(), + }) + .await + .expect("runtime mode port should update the Core owner"); + + assert_eq!( + session_manager + .get_session(&session.session_id) + .map(|session| session.agent_type.clone()) + .as_deref(), + Some("plan") + ); + let _ = std::fs::remove_dir_all(workspace_path); + } + #[tokio::test] async fn session_model_runtime_updates_the_real_core_session() { use bitfun_agent_runtime::sdk::{AgentRuntimeBuilder, AgentSessionModelUpdateRequest}; diff --git a/src/crates/assembly/core/src/service_agent_runtime.rs b/src/crates/assembly/core/src/service_agent_runtime.rs index 93533622c..5141e890d 100644 --- a/src/crates/assembly/core/src/service_agent_runtime.rs +++ b/src/crates/assembly/core/src/service_agent_runtime.rs @@ -7,8 +7,9 @@ use bitfun_agent_runtime::sdk::{ AgentEventSource, AgentInteractionResponsePort, AgentRuntime, AgentRuntimeBuilder, - AgentSessionForkPort, AgentSessionModelPort, AgentSessionModelUpdateRequest, - AgentSessionRestorePort, AgentSessionUsagePort, AgentTurnSettlementPort, RuntimeError, + AgentSessionForkPort, AgentSessionModePort, AgentSessionModelPort, + AgentSessionModelUpdateRequest, AgentSessionRestorePort, AgentSessionUsagePort, + AgentTurnSettlementPort, RuntimeError, }; use bitfun_runtime_ports::{ AgentDialogTurnPort, AgentDialogTurnRequest, AgentInputAttachment, AgentLifecycleDeliveryPort, @@ -398,6 +399,7 @@ fn agent_input_attachment_from_image_context(context: ImageContextData) -> Agent fn core_agent_runtime_builder( submission: Arc, session_management: Arc, + session_mode: Arc, session_model: Arc, session_restore: Arc, transcript_reader: Arc, @@ -410,6 +412,7 @@ fn core_agent_runtime_builder( AgentRuntimeBuilder::new() .with_submission_port(submission) .with_session_management_port(session_management) + .with_session_mode_port(session_mode) .with_session_model_port(session_model) .with_session_restore_port(session_restore) .with_session_transcript_reader(transcript_reader) @@ -791,6 +794,7 @@ impl CoreServiceAgentRuntime { ) -> Result { let submission: Arc = coordinator.clone(); let session_management: Arc = coordinator.clone(); + let session_mode: Arc = coordinator.clone(); let session_model: Arc = coordinator.clone(); let session_restore: Arc = coordinator.clone(); let transcript_reader: Arc = @@ -801,6 +805,7 @@ impl CoreServiceAgentRuntime { core_agent_runtime_builder( submission, session_management, + session_mode, session_model, session_restore, transcript_reader, @@ -819,6 +824,7 @@ impl CoreServiceAgentRuntime { let submission: Arc = coordinator.clone(); let session_management = scheduled_session_management_port(coordinator.clone(), scheduler.clone()); + let session_mode: Arc = coordinator.clone(); let session_model: Arc = coordinator.clone(); let session_restore: Arc = coordinator.clone(); let transcript_reader: Arc = @@ -831,6 +837,7 @@ impl CoreServiceAgentRuntime { core_agent_runtime_builder( submission, session_management, + session_mode, session_model, session_restore, transcript_reader, @@ -851,6 +858,7 @@ impl CoreServiceAgentRuntime { let submission: Arc = coordinator.clone(); let session_management = scheduled_session_management_port(coordinator.clone(), scheduler.clone()); + let session_mode: Arc = coordinator.clone(); let session_model: Arc = coordinator.clone(); let session_restore: Arc = coordinator.clone(); let transcript_reader: Arc = @@ -862,6 +870,7 @@ impl CoreServiceAgentRuntime { core_agent_runtime_builder( submission, session_management, + session_mode, session_model, session_restore, transcript_reader, @@ -881,6 +890,7 @@ impl CoreServiceAgentRuntime { let submission: Arc = coordinator.clone(); let session_management = scheduled_session_management_port(coordinator.clone(), scheduler.clone()); + let session_mode: Arc = coordinator.clone(); let session_model: Arc = coordinator.clone(); let session_restore: Arc = coordinator.clone(); let transcript_reader: Arc = @@ -893,6 +903,7 @@ impl CoreServiceAgentRuntime { core_agent_runtime_builder( submission, session_management, + session_mode, session_model, session_restore, transcript_reader, @@ -965,6 +976,7 @@ impl CoreServiceAgentRuntime { let submission: Arc = coordinator.clone(); let session_management = scheduled_session_management_port(coordinator.clone(), scheduler.clone()); + let session_mode: Arc = coordinator.clone(); let session_model: Arc = coordinator.clone(); let session_restore: Arc = coordinator.clone(); let transcript_reader: Arc = @@ -977,6 +989,7 @@ impl CoreServiceAgentRuntime { let builder = core_agent_runtime_builder( submission, session_management, + session_mode, session_model, session_restore, transcript_reader, diff --git a/src/crates/contracts/runtime-ports/AGENTS.md b/src/crates/contracts/runtime-ports/AGENTS.md index a9f1aed93..82afbbb89 100644 --- a/src/crates/contracts/runtime-ports/AGENTS.md +++ b/src/crates/contracts/runtime-ports/AGENTS.md @@ -19,6 +19,9 @@ facts. It is an interface crate, not a runtime implementation crate. - `SessionStorePort` owns typed session storage-path resolution plus restore / load request and timing facts only. Concrete session persistence, file IO, session lifecycle, context restore, and prompt assembly do not belong here. +- Session model/mode mutation ports carry only the selected identity to the + current owner. Catalog lookup, validity policy, persistence implementation, + and product presentation stay outside this crate. - `ScriptToolRuntime` owns only provider-neutral availability, versioned load/invoke/cancel/dispose requests, execution context paths, and string results. Ecosystem source parsing, approval/conflict policy, product routing, diff --git a/src/crates/contracts/runtime-ports/src/lib.rs b/src/crates/contracts/runtime-ports/src/lib.rs index 320a83a89..a99059928 100644 --- a/src/crates/contracts/runtime-ports/src/lib.rs +++ b/src/crates/contracts/runtime-ports/src/lib.rs @@ -1024,6 +1024,13 @@ pub struct AgentSessionModelUpdateRequest { pub model_id: String, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentSessionModeUpdateRequest { + pub session_id: String, + pub mode_id: String, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AgentSessionForkRequest { @@ -1744,6 +1751,11 @@ pub trait AgentSessionModelPort: Send + Sync { -> PortResult<()>; } +#[async_trait::async_trait] +pub trait AgentSessionModePort: Send + Sync { + async fn update_session_mode(&self, request: AgentSessionModeUpdateRequest) -> PortResult<()>; +} + #[async_trait::async_trait] pub trait AgentSessionForkPort: Send + Sync { async fn fork_session( @@ -2770,6 +2782,10 @@ mod tests { session_id: "session_1".to_string(), model_id: "provider/model".to_string(), }; + let mode_request = AgentSessionModeUpdateRequest { + session_id: "session_1".to_string(), + mode_id: "agentic".to_string(), + }; let workspace_request = AgentSessionWorkspaceRequest { session_id: "session_1".to_string(), }; @@ -2784,6 +2800,7 @@ mod tests { let summary_json = serde_json::to_value(summary).expect("serialize summary"); let delete_json = serde_json::to_value(delete_request).expect("serialize delete request"); let model_json = serde_json::to_value(model_request).expect("serialize model request"); + let mode_json = serde_json::to_value(mode_request).expect("serialize mode request"); let workspace_json = serde_json::to_value(workspace_request).expect("serialize workspace request"); let binding_json = @@ -2801,6 +2818,8 @@ mod tests { assert_eq!(delete_json["remoteSshHost"], "host-1"); assert_eq!(model_json["sessionId"], "session_1"); assert_eq!(model_json["modelId"], "provider/model"); + assert_eq!(mode_json["sessionId"], "session_1"); + assert_eq!(mode_json["modeId"], "agentic"); assert_eq!(workspace_json["sessionId"], "session_1"); assert_eq!(binding_json["workspaceId"], "workspace_1"); assert_eq!(binding_json["workspacePath"], "/workspace/project"); diff --git a/src/crates/execution/agent-runtime/src/runtime.rs b/src/crates/execution/agent-runtime/src/runtime.rs index f54183462..6bb81e566 100644 --- a/src/crates/execution/agent-runtime/src/runtime.rs +++ b/src/crates/execution/agent-runtime/src/runtime.rs @@ -14,16 +14,17 @@ use bitfun_runtime_ports::{ AgentInputAttachment, AgentLifecycleDeliveryPort, AgentSessionCreateRequest, AgentSessionCreateResult, AgentSessionDeleteRequest, AgentSessionForkPort, AgentSessionForkRequest, AgentSessionForkResult, AgentSessionListRequest, - AgentSessionManagementPort, AgentSessionModelPort, AgentSessionModelUpdateRequest, - AgentSessionSummary, AgentSessionUsagePort, AgentSessionUsageRequest, - AgentSessionWorkspaceBinding, AgentSessionWorkspaceRequest, AgentSubmissionPort, - AgentSubmissionRequest, AgentSubmissionResult, AgentSubmissionSource, - AgentThreadGoalCreateRequest, AgentThreadGoalDeliveryRequest, AgentThreadGoalGetRequest, - AgentThreadGoalManagementPort, AgentThreadGoalUpdateStatusRequest, AgentTurnCancellationPort, - AgentTurnCancellationRequest, AgentTurnCancellationResult, AgentTurnSettlementPort, - AgentTurnSettlementRequest, DialogSubmitOutcome, PluginRuntimeBinding, PortError, - PortErrorKind, PortResult, RuntimeEventEnvelope, SessionTranscript, SessionTranscriptReader, - SessionTranscriptRequest, ThreadGoal, + AgentSessionManagementPort, AgentSessionModePort, AgentSessionModeUpdateRequest, + AgentSessionModelPort, AgentSessionModelUpdateRequest, AgentSessionSummary, + AgentSessionUsagePort, AgentSessionUsageRequest, AgentSessionWorkspaceBinding, + AgentSessionWorkspaceRequest, AgentSubmissionPort, AgentSubmissionRequest, + AgentSubmissionResult, AgentSubmissionSource, AgentThreadGoalCreateRequest, + AgentThreadGoalDeliveryRequest, AgentThreadGoalGetRequest, AgentThreadGoalManagementPort, + AgentThreadGoalUpdateStatusRequest, AgentTurnCancellationPort, AgentTurnCancellationRequest, + AgentTurnCancellationResult, AgentTurnSettlementPort, AgentTurnSettlementRequest, + DialogSubmitOutcome, PluginRuntimeBinding, PortError, PortErrorKind, PortResult, + RuntimeEventEnvelope, SessionTranscript, SessionTranscriptReader, SessionTranscriptRequest, + ThreadGoal, }; use bitfun_runtime_services::RuntimeServices; @@ -187,6 +188,7 @@ pub trait RuntimeAgentRegistry: Send + Sync { pub struct AgentRuntime { submission: Arc, session_management: Option>, + session_mode: Option>, session_model: Option>, session_fork: Option>, session_usage: Option>, @@ -219,6 +221,13 @@ impl std::fmt::Debug for AgentRuntime { .as_ref() .map(|_| ""), ) + .field( + "session_mode", + &self + .session_mode + .as_ref() + .map(|_| ""), + ) .field( "session_model", &self @@ -346,6 +355,7 @@ where pub struct AgentRuntimeBuilder { submission: Option>, session_management: Option>, + session_mode: Option>, session_model: Option>, session_fork: Option>, session_usage: Option>, @@ -390,6 +400,11 @@ impl AgentRuntimeBuilder { self } + pub fn with_session_mode_port(mut self, port: Arc) -> Self { + self.session_mode = Some(port); + self + } + pub fn with_session_fork_port(mut self, port: Arc) -> Self { self.session_fork = Some(port); self @@ -496,6 +511,7 @@ impl AgentRuntimeBuilder { let Self { submission, session_management, + session_mode, session_model, session_fork, session_usage, @@ -524,6 +540,7 @@ impl AgentRuntimeBuilder { Ok(AgentRuntime { submission: submission.ok_or(RuntimeBuildError::MissingSubmissionPort)?, session_management, + session_mode, session_model, session_fork, session_usage, @@ -807,6 +824,22 @@ impl AgentRuntime { .map_err(RuntimeError::from) } + pub async fn update_session_mode( + &self, + request: AgentSessionModeUpdateRequest, + ) -> Result<(), RuntimeError> { + let session_mode = self.session_mode.as_ref().ok_or_else(|| { + RuntimeError::Port(PortError::new( + PortErrorKind::NotAvailable, + "agent session mode port is not registered", + )) + })?; + session_mode + .update_session_mode(request) + .await + .map_err(RuntimeError::from) + } + pub async fn fork_session( &self, request: AgentSessionForkRequest, @@ -1083,15 +1116,16 @@ mod tests { use bitfun_runtime_ports::{ AgentBackgroundResultRequest, AgentDialogTurnRequest, AgentLifecycleDeliveryPort, AgentSessionCreateResult, AgentSessionDeleteRequest, AgentSessionListRequest, - AgentSessionManagementPort, AgentSessionSummary, AgentSessionWorkspaceRequest, - AgentSubmissionResult, AgentThreadGoalDeliveryKind, AgentThreadGoalDeliveryRequest, - AgentThreadGoalManagementPort, AgentTurnCancellationResult, ClockPort, DialogQueuePriority, - DialogSubmissionPolicy, DialogSubmitOutcome, FileSystemPort, PermissionPort, - PluginDispatchEnvelope, PluginResponseEnvelope, PluginRuntimeAvailability, - PluginRuntimeClient, PluginRuntimeUnavailableReason, PortErrorKind, PortResult, - RuntimeEventSink, RuntimeEventType, RuntimeServiceCapability, SessionStorePort, - SessionTranscript, SessionTranscriptReader, SessionTranscriptRequest, ThreadGoal, - ThreadGoalStatus, TranscriptContent, TranscriptMessage, WorkspacePort, + AgentSessionManagementPort, AgentSessionModePort, AgentSessionModeUpdateRequest, + AgentSessionSummary, AgentSessionWorkspaceRequest, AgentSubmissionResult, + AgentThreadGoalDeliveryKind, AgentThreadGoalDeliveryRequest, AgentThreadGoalManagementPort, + AgentTurnCancellationResult, ClockPort, DialogQueuePriority, DialogSubmissionPolicy, + DialogSubmitOutcome, FileSystemPort, PermissionPort, PluginDispatchEnvelope, + PluginResponseEnvelope, PluginRuntimeAvailability, PluginRuntimeClient, + PluginRuntimeUnavailableReason, PortErrorKind, PortResult, RuntimeEventSink, + RuntimeEventType, RuntimeServiceCapability, SessionStorePort, SessionTranscript, + SessionTranscriptReader, SessionTranscriptRequest, ThreadGoal, ThreadGoalStatus, + TranscriptContent, TranscriptMessage, WorkspacePort, }; use bitfun_runtime_services::{test_support::FakeRuntimePort, RuntimeServicesBuilder}; @@ -1104,6 +1138,7 @@ mod tests { listed_sessions: Mutex>, deleted_sessions: Mutex>, restored_sessions: Mutex>, + mode_updates: Mutex>, transcript_requests: Mutex>, workspace_binding_requests: Mutex>, thread_goal_gets: Mutex>, @@ -1247,6 +1282,17 @@ mod tests { } } + #[async_trait::async_trait] + impl AgentSessionModePort for FakeAgentRuntimePorts { + async fn update_session_mode( + &self, + request: AgentSessionModeUpdateRequest, + ) -> PortResult<()> { + self.mode_updates.lock().unwrap().push(request); + Ok(()) + } + } + #[async_trait::async_trait] impl SessionTranscriptReader for FakeAgentRuntimePorts { async fn read_session_transcript( @@ -1838,6 +1884,57 @@ mod tests { assert_eq!(ports.transcript_requests.lock().unwrap().len(), 1); } + #[tokio::test] + async fn session_mode_update_delegates_to_registered_port() { + let ports = Arc::new(FakeAgentRuntimePorts::default()); + let runtime = AgentRuntimeBuilder::new() + .with_submission_port(ports.clone()) + .with_session_mode_port(ports.clone()) + .build() + .expect("runtime"); + + runtime + .update_session_mode(AgentSessionModeUpdateRequest { + session_id: "session_1".to_string(), + mode_id: "plan".to_string(), + }) + .await + .expect("update session mode"); + + assert_eq!( + ports.mode_updates.lock().unwrap().as_slice(), + &[AgentSessionModeUpdateRequest { + session_id: "session_1".to_string(), + mode_id: "plan".to_string(), + }] + ); + } + + #[tokio::test] + async fn session_mode_update_requires_registered_port() { + let ports = Arc::new(FakeAgentRuntimePorts::default()); + let runtime = AgentRuntimeBuilder::new() + .with_submission_port(ports) + .build() + .expect("runtime"); + + let error = runtime + .update_session_mode(AgentSessionModeUpdateRequest { + session_id: "session_1".to_string(), + mode_id: "plan".to_string(), + }) + .await + .unwrap_err(); + + assert!(matches!( + error, + RuntimeError::Port(PortError { + kind: PortErrorKind::NotAvailable, + .. + }) + )); + } + #[test] fn session_restore_contract_serializes_runtime_owned_state() { let request = AgentSessionRestoreRequest { diff --git a/src/crates/execution/agent-runtime/src/sdk.rs b/src/crates/execution/agent-runtime/src/sdk.rs index c96443fba..34ab0c7bc 100644 --- a/src/crates/execution/agent-runtime/src/sdk.rs +++ b/src/crates/execution/agent-runtime/src/sdk.rs @@ -59,18 +59,19 @@ pub use bitfun_runtime_ports::{ AgentInputAttachment, AgentLifecycleDeliveryPort, AgentSessionCreateRequest, AgentSessionCreateResult, AgentSessionDeleteRequest, AgentSessionForkPort, AgentSessionForkRequest, AgentSessionForkResult, AgentSessionListRequest, - AgentSessionManagementPort, AgentSessionModelPort, AgentSessionModelUpdateRequest, - AgentSessionSummary, AgentSessionUsagePort, AgentSessionUsageRequest, - AgentSessionWorkspaceBinding, AgentSessionWorkspaceRequest, AgentSubmissionPort, - AgentSubmissionRequest, AgentSubmissionResult, AgentSubmissionSource, - AgentThreadGoalCreateRequest, AgentThreadGoalDeliveryRequest, AgentThreadGoalGetRequest, - AgentThreadGoalManagementPort, AgentThreadGoalUpdateStatusRequest, AgentTurnCancellationPort, - AgentTurnCancellationRequest, AgentTurnCancellationResult, AgentTurnSettlementPort, - AgentTurnSettlementRequest, ClockPort, DialogSubmissionPolicy, DialogSubmitOutcome, - FileSystemPort, GitPort, McpCatalogPort, NetworkPort, PermissionDecision, PermissionPort, - PermissionRequest, PortError, PortErrorKind, PortResult, RemoteAssistantWorkspaceFacts, - RemoteCapabilityPort, RemoteConnectionPort, RemoteProjectionPort, RemoteRecentWorkspaceFacts, - RemoteWorkspaceFacts, RemoteWorkspaceFileRuntimeHost, RemoteWorkspaceKind, RemoteWorkspacePort, + AgentSessionManagementPort, AgentSessionModePort, AgentSessionModeUpdateRequest, + AgentSessionModelPort, AgentSessionModelUpdateRequest, AgentSessionSummary, + AgentSessionUsagePort, AgentSessionUsageRequest, AgentSessionWorkspaceBinding, + AgentSessionWorkspaceRequest, AgentSubmissionPort, AgentSubmissionRequest, + AgentSubmissionResult, AgentSubmissionSource, AgentThreadGoalCreateRequest, + AgentThreadGoalDeliveryRequest, AgentThreadGoalGetRequest, AgentThreadGoalManagementPort, + AgentThreadGoalUpdateStatusRequest, AgentTurnCancellationPort, AgentTurnCancellationRequest, + AgentTurnCancellationResult, AgentTurnSettlementPort, AgentTurnSettlementRequest, ClockPort, + DialogSubmissionPolicy, DialogSubmitOutcome, FileSystemPort, GitPort, McpCatalogPort, + NetworkPort, PermissionDecision, PermissionPort, PermissionRequest, PortError, PortErrorKind, + PortResult, RemoteAssistantWorkspaceFacts, RemoteCapabilityPort, RemoteConnectionPort, + RemoteProjectionPort, RemoteRecentWorkspaceFacts, RemoteWorkspaceFacts, + RemoteWorkspaceFileRuntimeHost, RemoteWorkspaceKind, RemoteWorkspacePort, RemoteWorkspaceRuntimeHost, RemoteWorkspaceUpdate, RuntimeEventEnvelope, RuntimeEventSink, RuntimeEventType, RuntimeServiceCapability, RuntimeServicePort, SessionStorageKind, SessionStoragePathRequest, SessionStoragePathResolution, SessionStorePort, SessionTranscript, @@ -123,6 +124,11 @@ impl AgentRuntimeBuilder { self } + pub fn with_session_mode_port(mut self, port: Arc) -> Self { + self.inner = self.inner.with_session_mode_port(port); + self + } + pub fn with_session_fork_port(mut self, port: Arc) -> Self { self.inner = self.inner.with_session_fork_port(port); self @@ -293,6 +299,13 @@ impl AgentRuntime { self.inner.update_session_model(request).await } + pub async fn update_session_mode( + &self, + request: AgentSessionModeUpdateRequest, + ) -> Result<(), RuntimeError> { + self.inner.update_session_mode(request).await + } + pub async fn fork_session( &self, request: AgentSessionForkRequest, diff --git a/src/crates/interfaces/acp/AGENTS-CN.md b/src/crates/interfaces/acp/AGENTS-CN.md index 637fe64e5..811c49b3c 100644 --- a/src/crates/interfaces/acp/AGENTS-CN.md +++ b/src/crates/interfaces/acp/AGENTS-CN.md @@ -7,8 +7,8 @@ `bitfun-acp` 负责基于已组装产品 runtime 的 Agent Client Protocol 入口与 ACP client 行为。ACP protocol / client 细节留在这里或应用入口 adapter 中;跨层只共享稳定 capability facts。 CLI 托管的 ACP 服务端已通过 `ProductAssembler` 消费 `DeliveryProfile::Acp`,并使用 Agent Runtime SDK -完成会话创建/列举、轮次提交/取消、交互响应和 Agent 事件订阅。`bitfun-acp` 仍直接依赖 -`bitfun-core/product-full`,用于一次性恢复完整持久化历史、模型/模式配置、模型目录与配置读取、MCP 配置, +完成会话创建/列举、活动会话模型/模式更新、轮次提交/取消、交互响应和 Agent 事件订阅。`bitfun-acp` 仍直接依赖 +`bitfun-core/product-full`,用于一次性恢复完整持久化历史、模型/模式目录与提供方配置读取、MCP 配置, 以及本 crate 的 ACP 客户端路径。在这些生产路径分别获得可移植替代并证明等价前,不得宣称整个 crate 已与 Core 解耦。 diff --git a/src/crates/interfaces/acp/AGENTS.md b/src/crates/interfaces/acp/AGENTS.md index 140a5bb43..77ba2bb99 100644 --- a/src/crates/interfaces/acp/AGENTS.md +++ b/src/crates/interfaces/acp/AGENTS.md @@ -10,12 +10,12 @@ share only stable capability facts through contract crates. The CLI-hosted ACP server consumes `DeliveryProfile::Acp` through `ProductAssembler` and uses the Agent Runtime SDK for session creation/listing, -dialog submission/cancellation, interaction responses, and agent event -subscription. `bitfun-acp` still depends directly on `bitfun-core` with -`product-full` for single-pass full persisted-history restore, model/mode configuration, -model catalog/config reads, MCP provisioning, and the ACP client half of this -crate. Do not describe the crate as Core-independent until those production -paths have separately proven portable replacements. +active session model/mode updates, dialog submission/cancellation, interaction +responses, and agent event subscription. `bitfun-acp` still depends directly on +`bitfun-core` with `product-full` for single-pass full persisted-history restore, +model/mode catalog and provider configuration reads, MCP provisioning, and the +ACP client half of this crate. Do not describe the crate as Core-independent +until those production paths have separately proven portable replacements. ## Guardrails diff --git a/src/crates/interfaces/acp/src/runtime/session.rs b/src/crates/interfaces/acp/src/runtime/session.rs index affb4c781..3f181efe0 100644 --- a/src/crates/interfaces/acp/src/runtime/session.rs +++ b/src/crates/interfaces/acp/src/runtime/session.rs @@ -7,7 +7,8 @@ use agent_client_protocol::schema::{ }; use agent_client_protocol::{Client, ConnectionTo, Error, Result}; use bitfun_agent_runtime::sdk::{ - AgentSessionCreateRequest, AgentSessionListRequest, SessionStoragePathRequest, + AgentSessionCreateRequest, AgentSessionListRequest, AgentSessionModeUpdateRequest, + SessionStoragePathRequest, }; use bitfun_core::agentic::agents::get_agent_registry; use chrono::{DateTime, Utc}; @@ -218,10 +219,13 @@ impl BitfunAcpRuntime { validate_mode_id(mode_id).await?; - self.compatibility - .update_session_agent_type(&bitfun_session_id, mode_id) + self.agent_runtime + .update_session_mode(AgentSessionModeUpdateRequest { + session_id: bitfun_session_id, + mode_id: mode_id.to_string(), + }) .await - .map_err(Self::internal_error)?; + .map_err(Self::runtime_error)?; if let Some(mut state) = self.sessions.get_mut(session_id) { state.mode_id = mode_id.to_string(); @@ -289,7 +293,7 @@ fn unix_ms_to_rfc3339(time_ms: u64) -> String { #[cfg(test)] mod tests { - use super::unix_ms_to_rfc3339; + use super::{unix_ms_to_rfc3339, validate_mode_id}; #[test] fn session_timestamps_remain_rfc3339_after_runtime_projection() { @@ -299,4 +303,9 @@ mod tests { "2023-11-14T22:13:20+00:00" ); } + + #[tokio::test] + async fn unknown_session_mode_remains_an_invalid_request() { + assert!(validate_mode_id("__missing_acp_mode__").await.is_err()); + } }