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
7 changes: 5 additions & 2 deletions docs/architecture/agent-runtime-deployment-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@ flowchart LR

| 范围 | 当前状态 |
|---|---|
| Embedded GUI/TUI/CLI/ACP/SDK Host | 保持现状;本设计没有改变其依赖或生命周期 |
| Embedded GUI/Headless CLI/ACP/SDK Host | 保持现状;本设计没有改变其依赖或生命周期 |
| Embedded interactive TUI | CLI crate 私有的 `CliAgentRuntimeClient` 统一暴露 Session、Turn、Permission 和事件访问;前三者使用 Rust Runtime SDK(当前 preview),事件继续复用既有 CLI event source;仍在当前 CLI 进程内运行 |
| Runtime ownership | 已有可选的 Embedded 共享锁 / Shared 独占锁原语;尚未接入产品入口 |
| Shared local IPC | 已有未发布、仅 crate 内可见的 discovery、实例锁、严格握手、Health 和 cleanup 基础;尚无生产 consumer |
| Shared Session/Turn/Tool/Permission | 尚未设计为稳定 wire,也没有产品 consumer |
| Shared GUI/TUI/Remote | 尚未交付,没有 `--shared` 或隐藏 Host 命令 |

因此当前新增的是基础设施,不是用户可用的 Shared Runtime 产品。
因此当前新增的是基础设施,不是用户可用的 Shared Runtime 产品。TUI 的私有 client 只收敛第一方调用边界,不代表事件已经迁入 Rust Runtime SDK(当前 preview),也不代表已经存在 Shared consumer。

## 2. 最少名词

Expand Down Expand Up @@ -140,6 +141,7 @@ flowchart LR
```

- CLI 不依赖 SDK Host,GUI/TUI 也不依赖公开 SDK package。
- 交互式 TUI 的启动页和会话页复用一个 CLI 私有 Runtime client;Session、Turn 和 Permission 使用 Rust Runtime SDK(当前 preview),事件继续使用既有 CLI event source。该 client 只是第一方 adapter,不是公开 SDK 或第二套 Runtime。
- TUI 不是 Server;未来是否连接 Shared deployment 是部署选择,不改变 TUI 的 renderer/键位职责。
- Agent SDK Host 只服务外部 SDK 合同,不成为第一方 rich-client 的通用底座。
- Headless CLI 默认继续 Embedded;CI 或测试可保持独立进程和独立 workspace,不承担后台实例成本。
Expand Down Expand Up @@ -199,5 +201,6 @@ Session/Turn、事件恢复、Permission/UserInput、Controller、配置管理
- Client、窗口、Session 或 workspace 数量不会自动等量增加 Runtime 或 Plugin Host 进程。
- 私有 IPC 不成为公开 SDK、Remote、Peer、HTTP 或浏览器协议。
- 默认 GUI/TUI/Headless CLI 在 Shared 产品能力正式交付前保持现有 Embedded 行为。
- Account/session cloud sync 仍使用既有 Core compatibility 边界,不属于 Shared Runtime 支持。
- Remote workspace 的文件、凭据、进程和 Runtime 位于目标执行域,禁止静默回落本机。
- 未经真实 consumer 验证的接口不进入 wire;当前唯一 operation 是 Health。
47 changes: 41 additions & 6 deletions src/apps/cli/src/agent/runtime_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ use std::sync::{Arc, RwLock};
use tokio::sync::Mutex;

use bitfun_agent_runtime::sdk::{
AgentDialogTurnRequest, AgentRuntime, AgentSessionCreateRequest, AgentSessionDeleteRequest,
AgentSessionForkRequest, AgentSessionForkResult, AgentSessionListRequest,
AgentSessionModeUpdateRequest, AgentSessionModelUpdateRequest, AgentSessionRestoreRequest,
AgentSessionUsageRequest, AgentTurnCancellationRequest, AgentTurnSettlementRequest,
AgentUserAnswersRequest, PortErrorKind, RuntimeError, SessionTranscript,
SessionTranscriptRequest, SessionUsageReport, AUTO_APPROVE_ASK_CONTEXT_KEY,
AgentDialogTurnRequest, AgentLocalCommandTurnRecordRequest, AgentRuntime,
AgentSessionCreateRequest, AgentSessionDeleteRequest, AgentSessionForkRequest,
AgentSessionForkResult, AgentSessionListRequest, AgentSessionModeUpdateRequest,
AgentSessionModelUpdateRequest, AgentSessionRestoreRequest, AgentSessionUsageRequest,
AgentTurnCancellationRequest, AgentTurnSettlementRequest, AgentUserAnswersRequest,
PermissionReply, PermissionRequest, PermissionRequestEventReceiver, PortErrorKind,
RuntimeError, SessionTranscript, SessionTranscriptRequest, SessionUsageReport,
AUTO_APPROVE_ASK_CONTEXT_KEY,
};
use bitfun_agent_runtime::user_questions::USER_INPUT_AVAILABLE_CONTEXT_KEY;
use bitfun_runtime_ports::{AgentSessionSummary, AgentSubmissionSource, DialogSubmissionPolicy};
Expand Down Expand Up @@ -122,6 +124,39 @@ impl CliAgentRuntimeClient {
&self.event_source
}

pub(crate) fn subscribe_permission_requests(
&self,
) -> std::result::Result<PermissionRequestEventReceiver, RuntimeError> {
self.runtime.subscribe_permission_requests()
}

pub(crate) fn pending_permission_requests(
&self,
) -> std::result::Result<Vec<PermissionRequest>, RuntimeError> {
self.runtime.pending_permission_requests()
}

pub(crate) async fn respond_permission(
&self,
request_id: &str,
reply: PermissionReply,
) -> Result<()> {
self.runtime
.respond_permission(request_id, reply)
.await
.map_err(|error| anyhow::anyhow!(error.into_message()))
}

pub(crate) async fn record_completed_local_command_turn(
&self,
request: AgentLocalCommandTurnRecordRequest,
) -> Result<()> {
self.runtime
.record_completed_local_command_turn(request)
.await
.map_err(|error| anyhow::anyhow!(error.into_message()))
}

pub(crate) fn set_approval_policy(&self, policy: CliApprovalPolicy) {
*self
.approval_policy
Expand Down
44 changes: 29 additions & 15 deletions src/apps/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ use anyhow::{anyhow, Result};
use bitfun_core::service::remote_connect::DeviceIdentity;
use clap::{CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum};
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::OnceLock;
use std::sync::{Arc, OnceLock};

use agent::runtime_client::CliAgentRuntimeClient;
use config::CliConfig;
use mcp_import::{McpImportCommand, McpImportOutputFormat};
use modes::chat::ChatMode;
Expand Down Expand Up @@ -728,6 +729,10 @@ async fn run_interactive(
BootstrapProfile::Interactive,
)
.await?;
let agent = Arc::new(CliAgentRuntimeClient::new(
runtime.as_ref(),
Some(workspace_path.clone()),
));
// 3.5 Restore persisted account session (if any)
if let Some(user_id) = account::try_restore_session().await {
tracing::info!("Restored account session for user {user_id}");
Expand All @@ -754,7 +759,7 @@ async fn run_interactive(
// 4. Show startup page (with full command support)
let mut startup_page = StartupPage::new(
config,
runtime.agent_runtime().clone(),
Arc::clone(&agent),
runtime.compatibility().clone(),
default_agent,
workspace.clone(),
Expand All @@ -779,7 +784,13 @@ async fn run_interactive(
// Use the current project workspace selected at process start.
let workspace = startup_page.workspace();
let config = startup_page.config().clone();
let mut chat_mode = ChatMode::new(config, agent_type, workspace, runtime.clone());
let mut chat_mode = ChatMode::new(
config,
agent_type,
workspace,
agent,
runtime.compatibility().clone(),
);
if let Some(session_id) = restore_session_id {
chat_mode = chat_mode.with_restore_session(session_id);
}
Expand Down Expand Up @@ -1143,16 +1154,13 @@ async fn run_interactive_with_session(
let mut terminal = ui::init_terminal()?;
ui::render_loading(&mut terminal, "Initializing system, please wait...")?;

let workspace = Some(runtime.workspace_root().to_string_lossy().to_string());
let sessions = runtime
.agent_runtime()
.list_sessions(bitfun_runtime_ports::AgentSessionListRequest {
workspace_path: runtime.workspace_root().to_string_lossy().to_string(),
remote_connection_id: None,
remote_ssh_host: None,
})
.await
.map_err(|error| anyhow::anyhow!(error.into_message()))?;
let workspace_path = runtime.workspace_root().to_path_buf();
let workspace = Some(workspace_path.to_string_lossy().to_string());
let agent = Arc::new(CliAgentRuntimeClient::new(
runtime.as_ref(),
Some(workspace_path),
));
let sessions = agent.list_sessions().await?;
let agent_type = sessions
.iter()
.find(|session| session.session_id == session_id)
Expand All @@ -1164,8 +1172,14 @@ async fn run_interactive_with_session(
)
})?;

let mut chat_mode = ChatMode::new(config, agent_type, workspace, runtime.clone())
.with_restore_session(session_id);
let mut chat_mode = ChatMode::new(
config,
agent_type,
workspace,
agent,
runtime.compatibility().clone(),
)
.with_restore_session(session_id);
let run_result = chat_mode.run(Some(terminal));

shutdown_mcp_servers().await;
Expand Down
15 changes: 5 additions & 10 deletions src/apps/cli/src/modes/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use crossterm::event::{
Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind,
};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::path::PathBuf;
use std::sync::{
mpsc::{self, Receiver, TryRecvError as MpscTryRecvError},
Arc,
Expand All @@ -32,7 +31,6 @@ use crate::actions::{
use crate::agent::runtime_client::CliAgentRuntimeClient;
use crate::chat_state::ChatState;
use crate::config::CliConfig;
use crate::runtime::CliRuntimeContext;
use crate::ui::agent_selector::{AgentItem, AgentSelectorAction};
use crate::ui::chat::{ChatView, MouseGestureOutcome};
use crate::ui::command_menu::{ExternalCommandProjection, NativeCommandCollisionProjection};
Expand Down Expand Up @@ -84,6 +82,7 @@ use bitfun_core::external_sources::{
ExternalToolCatalogEntry, ExternalToolRuntimeKind, NativePromptCommandDescriptor,
PromptCommandAvailability, EXTERNAL_SOURCE_CONTROL_SCHEMA_V1,
};
use bitfun_core::product_runtime::CoreAgentRuntimeCompatibility;
use bitfun_core::service::config::GlobalConfigManager;
use bitfun_core::service::session_usage::render_usage_report_markdown;
use bitfun_product_domains::external_sources::{ExternalSourceHealth, ExternalSourceScope};
Expand Down Expand Up @@ -191,7 +190,7 @@ pub(crate) struct ChatMode {
agent_type: String,
workspace: Option<String>,
agent: Arc<CliAgentRuntimeClient>,
runtime: Arc<CliRuntimeContext>,
compatibility: CoreAgentRuntimeCompatibility,
/// User-level default resolved from shared config for this TUI run.
auto_approve_ask_default: bool,
/// Temporary override for the current session only.
Expand Down Expand Up @@ -241,21 +240,17 @@ impl ChatMode {
config: CliConfig,
agent_type: String,
workspace: Option<String>,
runtime: Arc<CliRuntimeContext>,
agent: Arc<CliAgentRuntimeClient>,
compatibility: CoreAgentRuntimeCompatibility,
) -> Self {
let agent = Arc::new(CliAgentRuntimeClient::new(
runtime.as_ref(),
workspace.clone().map(PathBuf::from),
));

let keymap = ResolvedKeymap::new(&config.shortcuts);
Self {
config,
keymap,
agent_type,
workspace,
agent,
runtime,
compatibility,
auto_approve_ask_default: false,
auto_approve_ask_override: None,
restore_session_id: None,
Expand Down
2 changes: 1 addition & 1 deletion src/apps/cli/src/modes/chat/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ impl ChatMode {
) {
let workspace = self.workspace_path_for_sync(chat_state);
crate::account_sync::start_auto_sync_background(
self.runtime.compatibility().clone(),
self.compatibility.clone(),
is_first_login,
workspace,
);
Expand Down
4 changes: 2 additions & 2 deletions src/apps/cli/src/modes/chat/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ impl ChatMode {
match prompt.handle_key_event(key) {
PermissionAction::Reply(reply) => {
let request_id = prompt.request.request_id.clone();
let runtime = self.runtime.agent_runtime().clone();
let agent = Arc::clone(&self.agent);
let result = tokio::task::block_in_place(|| {
rt_handle.block_on(runtime.respond_permission(&request_id, reply))
rt_handle.block_on(agent.respond_permission(&request_id, reply))
});
match result {
Ok(()) => {
Expand Down
8 changes: 2 additions & 6 deletions src/apps/cli/src/modes/chat/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,8 @@ impl ChatMode {
}

let mut event_rx = self.agent.event_source().subscribe();
let mut permission_rx = self
.runtime
.agent_runtime()
.subscribe_permission_requests()
.ok();
if let Ok(pending) = self.runtime.agent_runtime().pending_permission_requests() {
let mut permission_rx = self.agent.subscribe_permission_requests().ok();
if let Ok(pending) = self.agent.pending_permission_requests() {
for request in pending.into_iter().filter(|request| {
crate::runtime::approval::permission_request_targets_session(request, &session_id)
}) {
Expand Down
8 changes: 2 additions & 6 deletions src/apps/cli/src/modes/chat/selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,12 @@ impl ChatMode {
.or_else(|| self.workspace.clone())
.or_else(|| Some(self.agent.workspace_path_string()));
let agent = self.agent.clone();
let runtime = Arc::clone(&self.runtime);

let report_result: Result<bitfun_core::service::session_usage::SessionUsageReport> =
tokio::task::block_in_place(|| {
let session_id = session_id.clone();
let workspace_path = workspace_path.clone();
let agent = agent.clone();
let runtime = Arc::clone(&runtime);
rt_handle.block_on(async move {
let workspace_path = workspace_path
.filter(|path| !path.trim().is_empty())
Expand All @@ -176,8 +174,7 @@ impl ChatMode {
let markdown = render_usage_report_markdown(&report);
let generated_at = u64::try_from(report.generated_at).unwrap_or_default();
let metadata = usage_report_metadata(&report)?;
runtime
.agent_runtime()
agent
.record_completed_local_command_turn(AgentLocalCommandTurnRecordRequest {
session_id,
content: markdown,
Expand All @@ -187,8 +184,7 @@ impl ChatMode {
anyhow!("Usage report metadata must be an object")
})?,
})
.await
.map_err(|error| anyhow!(error.into_message()))?;
.await?;

Ok(report)
})
Expand Down
Loading