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
3 changes: 2 additions & 1 deletion docs/architecture/agent-runtime-deployment-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ sequenceDiagram
end
```

当前私有协议(v9)只覆盖 TUI 已有用户旅程需要的窄操作:
当前私有协议(v10)只覆盖 TUI 已有用户旅程需要的窄操作:

| 已支持 | 明确不支持 |
|---|---|
Expand All @@ -228,6 +228,7 @@ sequenceDiagram
- JSON frame 使用 4-byte 长度前缀;request 在发送前执行 128 KiB 上限(覆盖 TUI 已有的 64 KiB 粘贴输入及类型化信封),response/event 在序列化时执行 8 MiB 上限。超限返回类型化错误,不能进行无界分配;超过该上限的历史 Session 暂由 Embedded TUI 打开,不在本阶段引入分页协议;
- 未认证连接也计入有界 connection budget,单个客户端不能无限制造 server task;
- 未知 frame/operation 信封字段、未知 operation、错误身份和不兼容版本 fail closed;复用的 Runtime DTO 按其既有反序列化契约处理字段;
- v10 增加两个只读、current-controller 限定的工作区引用 operation:按当前 Session 搜索文件/目录,以及按 user message ID 读取已持久化的结构化引用。两者复用 Agent Runtime 的 workspace-reference port,不赋予 IPC adapter 文件系统或 Session 持久化所有权,也不扩展为 Remote 或公开 SDK 协议。
- 一个连接最多控制一个 Session、同时最多提交一个活动 Turn;一个 Session 同时只有一个 controller。create/restore/fork 在完整结果通过大小检查后才原子切换控制权,失败时保留原 Session。fork 只接受当前 controller 的空闲 Session;无选中 Turn 时复制到最新持久化 Turn,指定 `before_turn_id` 时只复制该 Turn 之前的历史。活动 Turn 期间不能切换或 fork Session,也不能修改其名称、Agent mode 或 model;删除只作用于非当前且未被任何连接控制的 Session。
- Submit 与手动 context compaction 都使用调用方已有的 `turn_id` 标识不确定结果;若操作超时,返回 `outcome_unknown`、关闭连接并按该 ID 取消。手动 compaction 要求当前 controller 且 Session 空闲,由 Core 通过与普通对话 Turn 共用的原子准入路径创建一个可审计 maintenance Turn,并在取得所有权后读取压缩上下文:planning 阶段允许取消,atomic commit 开始后忽略晚到取消并保持 Processing 直至终态持久化完成。maintenance Turn 保留在权威 transcript 中但不进入模型上下文,live/restored payload 使用同一 compression ID 和 `applied` 事实;commit 后的持久化故障发布明确失败终态而不是遗留 Processing。断连取消只有得到确认后才释放 Session 控制权;无法确认时继续隔离该 Session,直到 Runtime 进程退出。
- Session delete/rename 和 Agent mode/model update 复用既有 Runtime 端口和校验,Runtime 对最终结果保持权威并拒绝无效目标。它们都是有副作用操作;发送前编码或 frame 上限失败表示请求未执行,连接仍可使用。rename 写入失败时恢复旧 metadata:确认恢复后返回明确失败,无法确认时返回 `outcome_unknown`。Shared Client 在请求写入后响应超时或丢失连接时也返回 `outcome_unknown` 并断开连接。两种情况都不自动重试:rename 由用户恢复 Session 并核对当前值;delete 由用户重新打开 `/sessions` 核对目标是否仍存在。模式与模型目录仍是同版本第一方产品事实,不加入 IPC。
Expand Down
2 changes: 2 additions & 0 deletions docs/architecture/cli-product-line-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ CLI-P1 应提供:

- 新建、恢复、继续、分叉、压缩和中断会话;所有动作使用同一 Session/Turn Runtime 语义。
- `@` 文件/目录引用和受控 `!` shell 请求;shell 仍进入工具、权限、取消和审计路径。
- TUI 的 `@` 入口遵循 OpenCode 交互:仅在输入开头或空白后触发,Enter 选择文件/目录,Tab 对目录继续下钻,文件可附带 `#start[-end]` 行范围;不新增平行斜杠命令。
- TUI 只维护 composer 文本、候选框和结构化引用位置。按 Session 绑定的搜索、路径/类型/行范围校验、持久化与历史恢复由 Agent Runtime 端口和 Core owner 负责;Core 不提前读取文件内容,Agent 仍通过既有 Read/Glob 工具进入权限、取消和审计路径。远程工作区当前返回明确的不可用原因,不回退到本地文件系统。
- 对话 checkpoint 与工作区 checkpoint 的独立事实;rewind 必须明确选择只回退对话、只回退工作区或两者。
- 后台 Agent/工具/工作流的状态、取消和结果回收,不允许无结果的隐式 detached task。
- 外部编辑器、命令历史、详情/用量视图、图片附件和终端能力降级。
Expand Down
2 changes: 1 addition & 1 deletion scripts/core-boundaries/rules/source/forbidden-rules.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const forbiddenContentRules = [
reason: 'agent-runtime-ipc operation scope is frozen to the reviewed Shared TUI slice',
patterns: [
{
regex: /^\s+(?!(?:Health|ListSessions|CreateSession|RestoreSession|DeleteSession|ForkSession|RenameSession|UpdateSessionMode|UpdateSessionModel|ReloadSessionContext|CompactSession|UndoSession|RedoSession|SubmitTurn|CancelTurn|PendingPermissions|RespondPermission|SubmitUserAnswers|Unit|Sessions|SessionCreated|SessionRestored|SessionForked|SessionReverted|TurnAccepted|TurnCancelled|None|CurrentController|AttachExisting|UncontrolledTarget|Self|RuntimeIpcSessionRequirement|RuntimeIpcOperationRules|RuntimeSessionForkRequest|AgentContextReloadRequest|AgentDialogTurnRequest|AgentSessionCompactionRequest|AgentSessionCreateRequest|AgentSessionCreateResult|AgentSessionListRequest|AgentSessionModeUpdateRequest|AgentSessionModelUpdateRequest|AgentSessionRevertRequest|AgentSessionRevertResult|AgentSessionSummary|AgentTurnCancellationRequest|AgentTurnCancellationResult|SessionTranscript)\b)[A-Z][A-Za-z0-9_]*\b/,
regex: /^\s+(?!(?:Health|ListSessions|CreateSession|RestoreSession|DeleteSession|ForkSession|RenameSession|UpdateSessionMode|UpdateSessionModel|ReloadSessionContext|CompactSession|UndoSession|RedoSession|SearchWorkspaceReferences|WorkspaceReferencesForMessage|SubmitTurn|CancelTurn|PendingPermissions|RespondPermission|SubmitUserAnswers|Unit|Sessions|SessionCreated|SessionRestored|SessionForked|SessionReverted|WorkspaceReferenceSearch|WorkspaceReferences|TurnAccepted|TurnCancelled|None|CurrentController|AttachExisting|UncontrolledTarget|Self|RuntimeIpcSessionRequirement|RuntimeIpcOperationRules|RuntimeSessionForkRequest|AgentContextReloadRequest|AgentDialogTurnRequest|AgentMessageWorkspaceReferencesRequest|AgentSessionCompactionRequest|AgentSessionCreateRequest|AgentSessionCreateResult|AgentSessionListRequest|AgentSessionModeUpdateRequest|AgentSessionModelUpdateRequest|AgentSessionRevertRequest|AgentSessionRevertResult|AgentSessionSummary|AgentTurnCancellationRequest|AgentTurnCancellationResult|AgentWorkspaceReference|AgentWorkspaceReferenceSearchRequest|AgentWorkspaceReferenceSearchResult|SessionTranscript)\b)[A-Z][A-Za-z0-9_]*\b/,
message:
'agent-runtime-ipc may not add archive, replay, observer, general controller-transfer, or other operations beyond the reviewed Shared TUI slice',
},
Expand Down
4 changes: 4 additions & 0 deletions scripts/core-boundaries/self-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4919,6 +4919,10 @@ export function runManifestParserSelfTest({
runtimeIpcOperationPattern.test(' UpdateSessionModel {') ||
runtimeIpcOperationPattern.test(' UndoSession {') ||
runtimeIpcOperationPattern.test(' RedoSession {') ||
runtimeIpcOperationPattern.test(' SearchWorkspaceReferences {') ||
runtimeIpcOperationPattern.test(' WorkspaceReferencesForMessage {') ||
runtimeIpcOperationPattern.test(' WorkspaceReferenceSearch {') ||
runtimeIpcOperationPattern.test(' WorkspaceReferences {') ||
runtimeIpcOperationPattern.test(' SubmitTurn {') ||
runtimeIpcOperationPattern.test(' SessionForked {') ||
runtimeIpcOperationPattern.test(' SessionReverted {')
Expand Down
89 changes: 79 additions & 10 deletions src/apps/cli/src/agent/runtime_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ use std::sync::{Arc, RwLock};
use tokio::sync::{broadcast, Mutex};

use bitfun_agent_runtime::sdk::{
AgentDialogTurnRequest, AgentEventReceiver, AgentLocalCommandTurnRecordRequest, AgentRuntime,
AgentSessionCompactionRequest, AgentSessionCreateRequest, AgentSessionDeleteRequest,
AgentSessionForkBeforeTurnRequest, AgentSessionForkRequest, AgentSessionForkResult,
AgentSessionListRequest, AgentSessionModeUpdateRequest, AgentSessionModelUpdateRequest,
AgentSessionRenameRequest, AgentSessionRestoreRequest, AgentSessionRevertRequest,
AgentSessionRevertResult, AgentSessionUsageRequest, AgentTurnCancellationRequest,
AgentTurnSettlementRequest, AgentUserAnswersRequest, PermissionReply, PermissionRequest,
AgentDialogTurnRequest, AgentEventReceiver, AgentLocalCommandTurnRecordRequest,
AgentMessageWorkspaceReferencesRequest, AgentRuntime, AgentSessionCompactionRequest,
AgentSessionCreateRequest, AgentSessionDeleteRequest, AgentSessionForkBeforeTurnRequest,
AgentSessionForkRequest, AgentSessionForkResult, AgentSessionListRequest,
AgentSessionModeUpdateRequest, AgentSessionModelUpdateRequest, AgentSessionRenameRequest,
AgentSessionRestoreRequest, AgentSessionRevertRequest, AgentSessionRevertResult,
AgentSessionUsageRequest, AgentTurnCancellationRequest, AgentTurnSettlementRequest,
AgentUserAnswersRequest, AgentWorkspaceReference, AgentWorkspaceReferenceSearchRequest,
AgentWorkspaceReferenceSearchResult, PermissionReply, PermissionRequest,
PermissionRequestEventReceiver, PortError, PortErrorKind, RuntimeError, SessionTranscript,
SessionTranscriptRequest, SessionUsageReport,
};
Expand All @@ -30,8 +32,9 @@ use bitfun_agent_runtime_ipc::{
};
use bitfun_events::{AgenticEvent, AgenticEventEnvelope};
use bitfun_runtime_ports::{
AgentSessionSummary, AgentSessionWorkspaceBinding, AgentSessionWorkspaceRequest,
AgentSubmissionSource, DialogSubmissionPolicy, SessionExecutionTarget,
put_agent_workspace_references, AgentSessionSummary, AgentSessionWorkspaceBinding,
AgentSessionWorkspaceRequest, AgentSubmissionSource, DialogSubmissionPolicy,
SessionExecutionTarget,
};

use crate::actions::SHARED_TUI_EMBEDDED_HANDOFF;
Expand Down Expand Up @@ -1117,6 +1120,16 @@ impl CliAgentRuntimeClient {
}

pub(crate) async fn send_message(&self, message: String, agent_type: &str) -> Result<String> {
self.send_message_with_workspace_references(message, Vec::new(), agent_type)
.await
}

pub(crate) async fn send_message_with_workspace_references(
&self,
message: String,
workspace_references: Vec<AgentWorkspaceReference>,
agent_type: &str,
) -> Result<String> {
let session_id = self.ensure_session(agent_type).await?;
tracing::info!("Sending message to session {}: {}", session_id, message);

Expand All @@ -1130,7 +1143,9 @@ impl CliAgentRuntimeClient {
}

// Start the dialog turn; events arrive through the shared broadcast source.
let metadata = approval_metadata(self.approval_policy());
let mut metadata = approval_metadata(self.approval_policy());
put_agent_workspace_references(&mut metadata, &workspace_references)
.map_err(|error| anyhow::anyhow!(error.message))?;
let request = AgentDialogTurnRequest {
session_id: session_id.clone(),
message: message.clone(),
Expand Down Expand Up @@ -1195,6 +1210,60 @@ impl CliAgentRuntimeClient {
submission
}

pub(crate) async fn search_workspace_references(
&self,
query: String,
) -> Result<AgentWorkspaceReferenceSearchResult> {
let session_id = self
.session_id
.lock()
.await
.clone()
.ok_or_else(|| anyhow::anyhow!("No active session"))?;
let request = AgentWorkspaceReferenceSearchRequest {
session_id,
query,
limit: 20,
};
match &self.backend {
CliAgentRuntimeBackend::Embedded(runtime) => runtime
.search_workspace_references(request)
.await
.map_err(|error| anyhow::anyhow!(error.into_message())),
CliAgentRuntimeBackend::Shared(client) => match client
.request(RuntimeIpcOperation::SearchWorkspaceReferences { request })
.await?
{
RuntimeIpcOperationResult::WorkspaceReferenceSearch { search } => Ok(search),
_ => Err(unexpected_shared_result("search_workspace_references")),
},
}
}

pub(crate) async fn workspace_references_for_message(
&self,
session_id: String,
message_id: String,
) -> Result<Vec<AgentWorkspaceReference>> {
let request = AgentMessageWorkspaceReferencesRequest {
session_id,
message_id,
};
match &self.backend {
CliAgentRuntimeBackend::Embedded(runtime) => runtime
.workspace_references_for_message(request)
.await
.map_err(|error| anyhow::anyhow!(error.into_message())),
CliAgentRuntimeBackend::Shared(client) => match client
.request(RuntimeIpcOperation::WorkspaceReferencesForMessage { request })
.await?
{
RuntimeIpcOperationResult::WorkspaceReferences { references } => Ok(references),
_ => Err(unexpected_shared_result("workspace_references_for_message")),
},
}
}

pub(crate) async fn cancel_current_turn(&self) -> Result<()> {
let session_id = self.session_id.lock().await.clone();
let turn_id = self.current_turn_id.lock().await.clone();
Expand Down
10 changes: 10 additions & 0 deletions src/apps/cli/src/chat_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ impl ChatMessage {

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SessionForkPoint {
pub message_id: String,
pub turn_id: String,
pub prompt: String,
pub timestamp: SystemTime,
Expand Down Expand Up @@ -492,6 +493,7 @@ impl ChatState {
.collect::<Vec<_>>()
.join("\n");
(!prompt.is_empty()).then_some(SessionForkPoint {
message_id: message.id.clone(),
turn_id,
prompt,
timestamp: message.timestamp,
Expand All @@ -500,6 +502,14 @@ impl ChatState {
.collect()
}

pub(crate) fn latest_user_message_id(&self) -> Option<String> {
self.messages
.iter()
.rev()
.find(|message| message.role == MessageRole::User)
.map(|message| message.id.clone())
}

pub(crate) fn set_worktree_control_available(&mut self, available: bool) {
self.worktree_control_available = available;
}
Expand Down
15 changes: 15 additions & 0 deletions src/apps/cli/src/modes/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,14 @@ struct PendingSessionOperation {
handle: tokio::task::JoinHandle<std::result::Result<(), SessionOperationError>>,
}

struct PendingWorkspaceReferenceSearch {
generation: u64,
query: String,
handle: tokio::task::JoinHandle<
std::result::Result<bitfun_agent_runtime::sdk::AgentWorkspaceReferenceSearchResult, String>,
>,
}

const SESSION_OPERATION_SLOW_NOTICE: Duration = Duration::from_secs(15);
const SHARED_TUI_CHAT_STATUS: &str = "Shared TUI preview: this view controls sessions, including deleting an idle Session, turns, the current Session name, current Session Agent mode, current Session model, and declarative context via /reload [skills|instructions]; model management remains Embedded, along with local extension, MCP, account-sync, and Agent/Subagent management.";

Expand Down Expand Up @@ -260,6 +268,9 @@ pub(crate) struct ChatMode {
/// One Session operation in flight. The event loop remains responsive while
/// the Runtime owner updates or deletes Session state.
pending_session_operation: Option<PendingSessionOperation>,
pending_workspace_reference_search: Option<PendingWorkspaceReferenceSearch>,
workspace_reference_search_generation: u64,
last_workspace_reference_query: Option<String>,
/// One explicit native slash-menu choice waiting for its parameterized submission.
selected_native_command_once: Option<String>,
external_source_snapshot: Option<ExternalSourceCatalogSnapshot>,
Expand Down Expand Up @@ -318,6 +329,9 @@ impl ChatMode {
pending_mcp_op: None,
pending_mcp_tasks: Vec::new(),
pending_session_operation: None,
pending_workspace_reference_search: None,
workspace_reference_search_generation: 0,
last_workspace_reference_query: None,
selected_native_command_once: None,
external_source_snapshot: None,
external_source_conflict_choices: BTreeMap::new(),
Expand Down Expand Up @@ -361,6 +375,7 @@ include!("chat/worktree.rs");
include!("chat/selection.rs");
include!("chat/mcp.rs");
include!("chat/sessions.rs");
include!("chat/workspace_references.rs");
include!("chat/capabilities.rs");
include!("chat/provider_models.rs");
include!("chat/tests.rs");
39 changes: 33 additions & 6 deletions src/apps/cli/src/modes/chat/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,28 @@ impl ChatMode {
let operation = if undo { "Undo" } else { "Redo" };
chat_view.set_status(Some(format!("{operation}ing session...")));
let agent = self.agent.clone();
let restored_workspace_references = if undo {
if let Some(message_id) = chat_state.latest_user_message_id() {
let session_id = chat_state.core_session_id.clone();
match tokio::task::block_in_place(|| {
rt_handle
.block_on(agent.workspace_references_for_message(session_id, message_id))
}) {
Ok(references) => Some(references),
Err(error) => {
chat_view.set_status(Some(format!(
"Could not prepare undo composer metadata: {error}"
)));
return;
}
}
} else {
None
}
} else {
None
};
let agent = self.agent.clone();
let result = tokio::task::block_in_place(|| {
rt_handle.block_on(async move { agent.revert_current_session(undo).await })
});
Expand All @@ -1115,7 +1137,12 @@ impl ChatMode {
);
match reverted.composer {
AgentSessionComposerUpdate::Preserve => {}
AgentSessionComposerUpdate::Replace { text } => chat_view.set_input(&text),
AgentSessionComposerUpdate::Replace { text } => {
chat_view.set_draft(crate::ui::workspace_reference::ComposerDraft {
text,
workspace_references: restored_workspace_references.unwrap_or_default(),
})
}
AgentSessionComposerUpdate::Clear => chat_view.clear_input(),
}
self.selected_native_command_once = None;
Expand Down Expand Up @@ -1221,7 +1248,7 @@ impl ChatMode {
if chat_state.is_processing {
if trimmed.starts_with('/') {
if let Some(input) = chat_view.send_input() {
return self.handle_command(&input, chat_view, chat_state, rt_handle);
return self.handle_command(&input.text, chat_view, chat_state, rt_handle);
}
} else if !trimmed.is_empty() {
chat_view.set_status(Some(
Expand All @@ -1233,11 +1260,11 @@ impl ChatMode {
}

if let Some(input) = chat_view.send_input() {
tracing::info!("User input: {}", input);
if input.starts_with('/') {
return self.handle_command(&input, chat_view, chat_state, rt_handle);
tracing::info!("User input: {}", input.text);
if input.text.starts_with('/') {
return self.handle_command(&input.text, chat_view, chat_state, rt_handle);
}
self.send_message_to_agent(input, chat_view, chat_state, rt_handle);
self.send_draft_to_agent(input, chat_view, chat_state, rt_handle);
}
Ok(None)
}
Expand Down
Loading
Loading