Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions docs/architecture/agent-runtime-services-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -693,9 +693,9 @@ pub struct HarnessExecutionContext {
通过 `assemble(ProductAssemblyInput)` 产生 `ProductRuntimeParts`,本文件不再为它定义第二套目标接口。

当前 CLI 入口已使用类型化 `RuntimeServices` 构造 `ProductRuntimeParts`,并通过一个调用级上下文把
Agent Runtime SDK、Harness、能力注册、调用级权限和 Agentic 事件广播交给 TUI、Exec、Session 与 Usage。
SDK 已承接会话创建/列举/删除、轮次提交和取消;SDK v1 尚未覆盖的恢复视图、消息、分支、用量和
工具确认由 `assembly/core` 的单一兼容门面转发。`doctor` 与 `health` 校验真实组装结果及必需注册完整性;
Agent Runtime SDK、Harness、能力注册、调用级权限和 Agentic 事件广播交给 TUI、Exec、Session、Usage 与
交互模式下的 Peer Host。SDK 已承接会话创建/列举/删除、轮次提交和取消;SDK v1 尚未覆盖的恢复视图、
消息、分支、用量、快照和工具确认由 `assembly/core` 的单一兼容门面转发。`doctor` 与 `health` 校验真实组装结果及必需注册完整性;
Core 的 Network、Git 和 MCP Catalog 当前仍含兼容 marker,因此该诊断不等于对这些外部服务做实时探活。

该切换仍是 `product-full` 兼容组装,不是 owner 迁移。协调器、调度器、持久化、工具管线和 Agentic Event Queue
Expand All @@ -713,7 +713,7 @@ ping 路由。未接入入口的 profile、枚举分支和单元测试仍不能

| 阶段 | 约束 |
|---|---|
| 当前 | CLI 消费真实 Runtime Parts 与 SDK,Core 兼容门面只承接 SDK v1 缺口;不扩张字段或再造描述符 |
| 当前 | CLI 与 Peer Host 消费真实 Runtime Parts 与 SDK,Core 兼容门面只承接 SDK v1 缺口;不扩张字段或再造描述符 |
| 迁移 | 迁移执行 owner 或继续接入 ACP、Desktop 前,必须分别证明行为等价;`assembly/core -> apps/relay-server` 反向依赖仍需消除 |
| 完成 | 每个声称支持的 profile 都由生产入口消费组装结果,并有最小入口验证;无消费方的 profile 不对外宣称可用 |

Expand Down Expand Up @@ -998,9 +998,10 @@ Product 测试:
- `bitfun-runtime-services` 提供类型化服务注入;工具 contracts、provider groups 与 execution 已分层。
- `bitfun-harness` 已提供类型化工作流描述与注册能力。
- `bitfun-core` 可继续作为 `product-full` 兼容门面,避免迁移期间一次性重写入口。
- CLI 已以 `DeliveryProfile::Cli` 构造真实 Runtime Parts 和 SDK runtime;本地 Agent 入口、会话和用量
共用一个调用级上下文与广播事件源,审批策略不再写回全局配置。Peer Host 仍保留既有 Core 兼容路径,
其协议与生命周期切换由独立变更处理。
- CLI 已以 `DeliveryProfile::Cli` 构造真实 Runtime Parts 和 SDK runtime;本地 Agent 入口、会话、用量和
Peer Host 共用一个调用级上下文与广播事件源,审批策略不再写回全局配置。Peer Host 通过 SDK 提交/取消
turn,通过单一 Core 兼容门面处理会话与快照缺口,不再构造独立调度器、持久化 manager 或事件队列;
wire schema、Relay ACK/重放和重连协议未在该切换中扩张。
- CLI 通过 SDK 处理已覆盖的 session/turn/cancel 操作,并通过一个 Core 兼容门面处理 SDK v1 缺口;
该门面复用现有 owner,不建立第二套状态或事件 schema。

Expand Down
23 changes: 17 additions & 6 deletions docs/architecture/cli-product-line-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,10 @@ BitFun CLI 应成为可独立安装和发布的 Agent 产品,而不是 Desktop
- BitFun 原生插件目录的发现、内容校验、来源确认,以及 OpenCode custom tool 静态名称预览。
- CLI 本地 Agent 入口以类型化 `RuntimeServices` 调用 `ProductAssembler`,选择 `DeliveryProfile::Cli`,
并把 `ProductRuntimeParts`、Agent Runtime SDK、事件源和调用级审批策略保存在一个 `CliRuntimeContext` 中。
- TUI、`exec`、会话和用量复用同一上下文。SDK 已承接会话创建/列举/删除、轮次提交和取消;
SDK v1 尚未覆盖的固定 ID、恢复视图、消息、分支、用量和工具确认由一个 Core 兼容门面转发给原 owner。
- Agentic Event Queue 仍是唯一事件 owner;TUI 与 `exec` 使用独立广播订阅,不互相消费事件。
- TUI、`exec`、会话、用量和交互模式下的 Peer Host 复用同一上下文。SDK 已承接会话创建/列举/删除、
轮次提交和取消;SDK v1 尚未覆盖的固定 ID、恢复视图、消息、分支、用量、快照和工具确认由一个 Core
兼容门面转发给原 owner。
- Agentic Event Queue 仍是唯一事件 owner;TUI、`exec` 与 Peer Host 使用独立广播订阅,不互相消费事件。
- 有界旧队列只承担兼容存储;达到容量时不得抑制广播。CLI 保持一个后台 drain,订阅方一旦报告 lag/closed,
必须取消活动 turn 并显式失败,不能在状态不完整时继续报告成功。
- 会话 ID 在进入存储路径前统一校验;运行时索引同时绑定 ID 与规范化存储路径,并以待提交 claim 计数保护
Expand All @@ -101,14 +102,24 @@ BitFun CLI 应成为可独立安装和发布的 Agent 产品,而不是 Desktop
关闭输入捕获、关闭 raw mode 并显示光标。真实 PTY/ConPTY 故障注入仍需独立验收。
- 初始化按入口分级:交互模式启动 Peer Host 与 MCP,`exec` 只启动 MCP;本地 session 管理和 usage 查询不启动
Peer Host/MCP。该分级不改变 Agentic/Terminal owner,也不等同于管理命令已有独立轻量 Runtime。
- Peer Host 的 HostInvoke、Relay、控制器身份、确认和重连协议仍走既有兼容路径,不属于本次本地 Runtime 切换。
- Peer Host 保持既有 HostInvoke / DeviceEvent wire schema 与 Relay 路由,但执行已接入上述调用级上下文:
对话提交和精确取消走 SDK,会话与快照缺口走单一 Core 兼容门面。Peer Host 只跟踪由 Peer 提交的根 turn、
其子 turn 与待确认工具;可确认工具始终由控制器确认,即使宿主全局策略跳过确认,Agent 也会暂停等待控制器。
该 Peer 专属确认要求会沿精确后台结果 follow-up 保留。后台结果按 Core 内部元数据中的精确父 turn 与来源子 turn
继承 ownership;仅在父 turn 仍运行时注入,否则排在
无关 turn 之后并保留 Peer ownership。来源 turn 完成而结果仍等待会话串行化时,仅保留有界、一次性内部 tombstone;
会话清理或事件流中断会移除它。最后一个控制器离线
或分离、事件订阅 lag/closed 时取消这些 turn;事件失步同时投递既有失败 terminal event,终态在实际发送尝试前
不提前清理,队列关闭时改走同一直接投递路径。事件以入队时的控制器快照为上界,每个目标发送前再确认仍连接;
单目标投递租约将分离或离线移除与本地 Relay 入队尝试串行化。显式断开无法确认取消时,本地界面仍安全退出并
显示警告。不承诺本次变更范围外的 ACK、重放或重连恢复。
- `doctor` 与 `health` 构造并校验真实 Runtime Parts,区分 assembly-ready、Core compatibility owner 和不可用扩展。
它们证明必需能力已注册,不把 Core 的 Network/Git/MCP compatibility marker 描述为外部服务实时可用。
- 独立 CLI 测试与打包工作流;主 CI 的三平台 workspace check 同时覆盖 `bitfun-cli` 编译。

上述切换不等于运行时 owner 已迁移,也不表示 CLI-P0 全部完成。CLI crate 仍以 `bitfun-core/product-full`
承载协调器、调度器、持久化、工具管线和部分 SDK v1 缺口;ACP stdio 仍走原入口,插件命令仍以来源管理和
静态预览为主。兼容门面只转发,不重新计算或写入同一事实。
承载协调器、调度器、持久化、工具管线和部分 SDK v1 缺口,但 Peer Host 不再自行构造这些 owner;ACP stdio
仍走原入口,插件命令仍以来源管理和静态预览为主。兼容门面只转发,不重新计算或写入同一事实。

目标态仍存在以下结构缺口:

Expand Down
31 changes: 28 additions & 3 deletions docs/architecture/peer-device-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,37 @@ FS) and must not be mixed with Peer Device Mode.
- Peer: decrypt → allow/deny → execute on the peer host:
- Desktop: webview bridge `peer-host-invoke://request` → same Tauri handlers
as local UI → `peer_host_invoke_complete`
- CLI: Core HostInvoke registry (`WorkspaceService`, FS, config, session,
git, `DialogScheduler`) — no webview. Desktop-only surfaces (MiniApp /
cron / ACP list) return empty or no-op so hydrate does not fail.
- CLI: the invocation-scoped CLI product runtime handles dialog submit/cancel
through the Agent Runtime SDK and session/snapshot gaps through one Core
compatibility facade — no webview and no second scheduler, persistence
manager, or event queue. Desktop-only surfaces (MiniApp / cron / ACP list)
return empty or no-op so hydrate does not fail.
- Events: peer agentic projection (and other product events such as terminal /
FS / MCP interaction) fan-out as `RemoteCommand::DeviceEvent` to attached
controllers; controller re-emits the same event names locally.
- CLI Peer Host forwards only turns submitted through Peer Host and linked
child turns. A background-result follow-up inherits ownership only when its
Core-internal metadata identifies the exact tracked parent and source child
turns; if an unrelated turn is running in the same session, the result queues
behind it without losing Peer ownership. Completed source lineage uses a
bounded, one-shot tombstone while delivery waits on session serialization;
session drain or event-stream interruption clears it. Peer Host
requires an attached controller before submit and binds tool confirmation to
the exact observed tool and turn. Confirmable Peer tools always wait for the
controller even when the host's global policy skips confirmation, so an Agent
pauses until the controller responds; exact background-result follow-ups
retain this Peer-only confirmation requirement. The host cancels tracked turns when the
last controller detaches/goes offline or the agent-event subscription
lags/closes; continuity loss also projects the existing dialog-turn-failed
terminal event. Terminal ownership remains tracked until the event reaches the
delivery attempt, and a closed local delivery queue uses the same direct
DeviceEvent path. Delivery targets are captured when an event is queued and
rechecked against the currently attached set before each send. A per-target
delivery lease serializes detach or offline removal with the local Relay
enqueue attempt. An explicit disconnect still restores the local controller
UI, but reports a warning when host cancellation was not confirmed. This
boundary does not change the Relay envelope or add ACK, replay, or reconnect
recovery.
- Relay `POST /api/devices/:id/rpc` waits up to **120s** for the peer response;
reverse proxies in front of the relay must use a matching (or higher) read
timeout or they will return 504 first.
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/product-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ flowchart LR
| 产品形态 | 当前 P0 插件能力 | 入口行为 |
|---|---|---|
| Desktop / product-full | 生产入口仍直接依赖 `bitfun-core/product-full`;当前没有 managed-plugin 管理或 OpenCode 静态预览的生产 UI/调用方 | 共享代码可编译不等于 Desktop 已消费插件能力 |
| CLI | 入口仍以 `bitfun-core/product-full` 作为执行兼容 owner;只为 BitFun 原生包提供来源审核、启用预览、精确内容确认和停用 | 本地 Agent 路径选择 `DeliveryProfile::Cli`,校验必需 Runtime Service 注册并消费 Runtime Parts/SDK;SDK 缺口由单一 Core 兼容门面转发。部分注册仍是 compatibility marker,不代表实时探活;插件 binding 明确禁用,不执行 OpenCode 插件代码 |
| CLI | 入口仍以 `bitfun-core/product-full` 作为执行兼容 owner;只为 BitFun 原生包提供来源审核、启用预览、精确内容确认和停用 | 本地 Agent 与 Peer Host 路径选择 `DeliveryProfile::Cli`,校验必需 Runtime Service 注册并消费同一 Runtime Parts/SDK;SDK 缺口由单一 Core 兼容门面转发。Peer Host 不再构造第二套调度、持久化或事件 owner。部分注册仍是 compatibility marker,不代表实时探活;插件 binding 明确禁用,不执行 OpenCode 插件代码 |
| ACP | 生产入口仍直接依赖 `bitfun-core/product-full` | `DeliveryProfile::Acp` 尚未进入入口组装;不得把测试中的 profile 解释为生产隔离 |
| Server / Remote | 当前生产路由没有插件状态消费闭环;Remote 插件执行未实现 | 不在本地替远端项目发现、准备或执行插件;未接入时返回明确不支持 |
| Web / Mobile Web | 依赖现有后端入口,不持有插件执行单元 | 对应 profile 当前为空计划或未接入生产,不能据枚举值宣称独立产品能力 |
Expand Down
4 changes: 3 additions & 1 deletion src/apps/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ The local Agent paths build the CLI product profile once per invocation. Interac
session commands, and usage reports use that invocation-scoped runtime context and event source.
Local management queries do not start Peer Host or MCP; `exec` starts MCP but not Peer Host.
Core remains the compatibility owner for execution and persistence operations not yet covered by the
Agent Runtime SDK; plugin execution is not enabled by this assembly path.
Agent Runtime SDK. When interactive mode enables Peer Host, Peer dialog submission, cancellation,
and agent-event fan-out reuse the same runtime context; Peer Host does not construct another
scheduler, persistence manager, or event queue. Plugin execution is not enabled by this assembly path.

## Common commands

Expand Down
25 changes: 24 additions & 1 deletion src/apps/cli/src/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,9 @@ async fn spawn_device_routing(relay_url: &str, device_name: &str) -> Result<()>
while let Some(event) = event_rx.recv().await {
handle_relay_event(event, &session_arc, &relay_client_arc).await;
}
if is_current_routing_client(&relay_client_arc).await {
crate::peer_host::update_controller_presence(Vec::new()).await;
}
tracing::info!("Device routing event loop exited");
});

Expand All @@ -249,9 +252,19 @@ async fn spawn_device_routing(relay_url: &str, device_name: &str) -> Result<()>

/// Disconnect the device-routing connection (if any).
pub(crate) async fn stop_device_routing() {
if let Some(client) = device_relay_client().write().await.take() {
let client = { device_relay_client().write().await.take() };
if let Some(client) = client {
client.disconnect().await;
}
crate::peer_host::update_controller_presence(Vec::new()).await;
}

async fn is_current_routing_client(client: &Arc<RelayClient>) -> bool {
device_relay_client()
.read()
.await
.as_ref()
.is_some_and(|current| Arc::ptr_eq(current, client))
}

/// Log out: tear down routing, revoke the token (best-effort), clear state.
Expand All @@ -277,16 +290,25 @@ async fn handle_relay_event(
session_arc: &Arc<RwLock<Option<AccountSession>>>,
relay_client: &Arc<RelayClient>,
) {
if !is_current_routing_client(relay_client).await {
tracing::debug!("Ignoring event from a stale device routing client");
return;
}
match event {
RelayEvent::AuthOk { user_id, device_id } => {
tracing::info!("Device routing auth ok: user={user_id} device={device_id}");
}
RelayEvent::AuthError { message } => {
tracing::warn!("Device routing auth error: {message}");
TOKEN_EXPIRED.store(true, Ordering::Relaxed);
crate::peer_host::update_controller_presence(Vec::new()).await;
}
RelayEvent::DevicePresence { devices } => {
tracing::info!("Device presence updated: {} online", devices.len());
crate::peer_host::update_controller_presence(
devices.into_iter().map(|device| device.device_id).collect(),
)
.await;
}
RelayEvent::DeviceMessageReceived {
source_device_id,
Expand Down Expand Up @@ -365,6 +387,7 @@ async fn handle_relay_event(
}
RelayEvent::Disconnected => {
tracing::info!("Device routing disconnected");
crate::peer_host::update_controller_presence(Vec::new()).await;
}
RelayEvent::Reconnected => {
tracing::info!("Device routing reconnected");
Expand Down
2 changes: 1 addition & 1 deletion src/apps/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ async fn initialize_core_services(
);

if bootstrap_profile.starts_peer_host() {
if let Err(e) = peer_host::ensure_peer_host_ready(runtime.agentic_system()).await {
if let Err(e) = peer_host::ensure_peer_host_ready(runtime.as_ref()).await {
tracing::warn!("Failed to initialize CLI peer host services: {e}");
} else {
tracing::info!("CLI peer host services initialized");
Expand Down
40 changes: 9 additions & 31 deletions src/apps/cli/src/peer_host/bootstrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,20 @@
use std::sync::Arc;

use anyhow::{Context, Result};
use bitfun_core::agentic::coordination::{self, DialogScheduler};
use bitfun_core::agentic::system::AgenticSystem;
use bitfun_core::infrastructure::try_get_path_manager_arc;
use bitfun_core::service::filesystem::FileSystemServiceFactory;
use bitfun_core::service::workspace::{self, WorkspaceService};

use crate::runtime::CliRuntimeContext;

use super::fanout::start_peer_event_fanout;
use super::state::{set_peer_host_state, try_peer_host_state, PeerHostState};
use super::state::{set_peer_host_state, try_peer_host_state, PeerHostState, PeerTurnTracker};

/// Ensure Peer Host services are ready. Idempotent.
pub(crate) async fn ensure_peer_host_ready(agentic: &AgenticSystem) -> Result<()> {
pub(crate) async fn ensure_peer_host_ready(runtime: &CliRuntimeContext) -> Result<()> {
if try_peer_host_state().is_some() {
return Ok(());
}

let path_manager = try_get_path_manager_arc().context("path manager")?;
let persistence = Arc::new(
bitfun_core::agentic::persistence::PersistenceManager::new(path_manager)
.context("persistence manager")?,
);

let scheduler = if let Some(existing) = coordination::get_global_scheduler() {
existing
} else {
let session_manager = agentic.coordinator.get_session_manager().clone();
let scheduler = DialogScheduler::new(agentic.coordinator.clone(), session_manager);
agentic
.coordinator
.set_scheduler_notifier(scheduler.outcome_sender());
agentic
.coordinator
.set_round_injection_source(scheduler.round_injection_monitor());
coordination::set_global_scheduler(scheduler.clone());
scheduler
};

let workspace_service = if let Some(existing) = workspace::get_global_workspace_service() {
existing
} else {
Expand All @@ -54,20 +32,20 @@ pub(crate) async fn ensure_peer_host_ready(agentic: &AgenticSystem) -> Result<()
let filesystem_service = Arc::new(FileSystemServiceFactory::create_default());

let state = PeerHostState {
coordinator: agentic.coordinator.clone(),
scheduler,
event_queue: agentic.event_queue.clone(),
agent_runtime: runtime.agent_runtime().clone(),
compatibility: runtime.compatibility().clone(),
agent_events: runtime.agent_events().clone(),
turns: PeerTurnTracker::new(),
workspace_service,
filesystem_service,
persistence,
};

if set_peer_host_state(state.clone()).is_err() {
// Another task won the race; treat as success.
return Ok(());
}

start_peer_event_fanout(state.event_queue.clone());
start_peer_event_fanout(state);
tracing::info!("CLI peer host services ready");
Ok(())
}
Loading
Loading