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
39 changes: 31 additions & 8 deletions docs/architecture/agent-runtime-deployment-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ flowchart TB
| Session 写入 | BitFun Runtime 的持久化 Session 由 `SessionManager` 管理;同一存储位置中的同一 Session 同时只允许一个本机进程写入,list/view 等只读操作不受影响 |
| 当前 HTTP Server | 只提供 health/info/WebSocket 外壳,未装配 Agent Runtime,因此不取得 workspace ownership;`bootstrap.rs` 仅保持 agent-enabled composition 的一致边界,不由当前入口启动 |
| Shared local IPC | 未发布的本机协议已有 discovery、实例锁、严格握手、Session 控制权、有界事件流和 cleanup;唯一 consumer 是第一方交互式 TUI adapter |
| Shared TUI | `bitfun --shared` / `bitfun chat --shared` 可列出、创建、恢复和重命名当前 Session,读取 transcript,切换当前 Session 的 Agent mode/model,通过 `/reload [skills|instructions]` 刷新声明式上下文,提交/取消 Turn,处理 Permission 和 UserInput;默认仍是 Embedded |
| Shared GUI/Headless/ACP/SDK Host/Remote | 未交付,也不会由 `--shared` 隐式启用;Replay、Observer、Controller transfer、Session delete/fork 同样不在当前协议中 |
| Shared TUI | `bitfun --shared` / `bitfun chat --shared` 可列出、创建、恢复 Session,删除未被控制的空闲非当前 Session,重命名当前 Session,读取 transcript,切换当前 Session 的 Agent mode/model,通过 `/reload [skills|instructions]` 刷新声明式上下文,提交/取消 Turn,处理 Permission 和 UserInput;默认仍是 Embedded |
| Shared GUI/Headless/ACP/SDK Host/Remote | 未交付,也不会由 `--shared` 隐式启用;Replay、Observer、Controller transfer、Session archive/fork 同样不在当前协议中 |

因此当前交付的是一条窄的、显式启用的 Shared TUI deployment,不是通用本机 Server。具体 `EventQueue` 仍由 Core 产品装配;IPC 只把当前 TUI 必需的强类型操作和事件映射到同一个 Runtime owner,没有事件重放或公开协议承诺。

Expand Down Expand Up @@ -196,19 +196,21 @@ sequenceDiagram
S-->>C: initialized(health + interactive_tui)
C->>S: create or restore Session
S-->>C: Session control + Session facts
C->>S: rename, update, or reload current Session context
C->>S: rename or update current Session
C->>S: delete idle non-current Session
C->>S: reload current Session context
C->>S: submit/cancel Turn or answer Permission/UserInput
S-->>C: Session-filtered authoritative events
else invalid
S-->>C: typed error and close
end
```

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

| 已支持 | 明确不支持 |
|---|---|
| Health、Session list/create、原子 restore(含 transcript 与 pending Permission)、当前 Session rename、Agent mode/model update、声明式上下文 reload | Session delete/fork、跨 workspace attach、transcript 分页、模型目录/默认值和 Agent/Subagent 管理 |
| Health、Session list/create、原子 restore(含 transcript 与 pending Permission)、删除未被控制的空闲 Session、当前 Session rename、Agent mode/model update、声明式上下文 reload | Session archive/fork、跨 workspace attach、transcript 分页、模型目录/默认值和 Agent/Subagent 管理 |
| Turn submit/cancel | replay、cursor、resume event stream |
| pending/respond Permission、submit UserInput answers | observer、controller transfer、多 Session multiplex |
| 连接断开清理、Session-filtered events | detach/observer/controller transfer、SDK callbacks、GUI/Remote/Peer/ACP/Headless wire |
Expand All @@ -225,9 +227,9 @@ 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 按其既有反序列化契约处理字段;
- 一个连接最多控制一个 Session、同时最多提交一个活动 Turn;一个 Session 同时只有一个 controller。create/restore 在完整结果通过大小检查后才原子切换控制权,失败时保留原 Session。活动 Turn 期间不能切换 Session,也不能修改其名称、Agent mode 或 model。
- 一个连接最多控制一个 Session、同时最多提交一个活动 Turn;一个 Session 同时只有一个 controller。create/restore 在完整结果通过大小检查后才原子切换控制权,失败时保留原 Session。活动 Turn 期间不能切换 Session,也不能修改其名称、Agent mode 或 model;删除只作用于非当前且未被任何连接控制的 Session
- Submit 使用调用方已有的 `turn_id` 标识不确定结果;若提交超时,返回 `outcome_unknown`、关闭连接并按该 ID 取消。断连取消只有得到确认后才释放 Session 控制权;无法确认时继续隔离该 Session,直到 Runtime 进程退出。
- Session rename 和 Agent mode/model update 复用既有 Runtime 端口和校验,Runtime 对最终更新保持权威并拒绝无效值。它们都是有副作用操作;发送前编码或 frame 上限失败表示请求未执行,连接仍可使用。rename 写入失败时恢复旧 metadata:确认恢复后返回明确失败,无法确认时返回 `outcome_unknown`。Shared Client 在请求写入后响应超时或丢失连接时也返回 `outcome_unknown` 并断开连接。两种情况都不自动重试;用户恢复 Session 并核对当前值后再决定是否重试。模式与模型目录仍是同版本第一方产品事实,不加入 IPC。
- Session delete/rename 和 Agent mode/model update 复用既有 Runtime 端口和校验,Runtime 对最终结果保持权威并拒绝无效目标。它们都是有副作用操作;发送前编码或 frame 上限失败表示请求未执行,连接仍可使用。rename 写入失败时恢复旧 metadata:确认恢复后返回明确失败,无法确认时返回 `outcome_unknown`。Shared Client 在请求写入后响应超时或丢失连接时也返回 `outcome_unknown` 并断开连接。两种情况都不自动重试:rename 由用户恢复 Session 并核对当前值;delete 由用户重新打开 `/sessions` 核对目标是否仍存在。模式与模型目录仍是同版本第一方产品事实,不加入 IPC。
- 声明式上下文 reload 只失效当前 Session 的 instructions 缓存,并按目标复用 Skill Registry 刷新;它可在活动 Turn 中执行但不改写该 Turn,generation 保护保证下一条消息重建上下文。它不引入 watcher、热替换或第二套 Runtime owner。
- Shared TUI 的模型选择器复用 Client 已有的只读产品配置来显示同版本模型目录;它只把选中的 model ID 通过 `update current Session model` 交给 Runtime。Client 不持有 Session 写入权,也不通过 IPC 管理模型目录或默认值。
- Agent 事件流 lag/closed 后 fail closed;Permission lag 先从 Runtime 权威 pending 集合重建,重建失败或流关闭时取消当前 Turn 并退出。路由到父 Session 的嵌套 Permission 与 AskUserQuestion 复用现有 TUI 交互,不新增第二套 UI 状态。
Expand Down Expand Up @@ -341,6 +343,27 @@ sequenceDiagram

Embedded 和 Shared 最终调用同一 `AgentRuntime::rename_session`。Runtime 只有在确认旧名称已保留时才返回明确失败;持久化恢复无法确认时,两种部署都返回 `outcome_unknown`。Shared 还会在请求已发送但权威响应丢失时返回该结果并关闭连接。用户恢复 Session、检查当前名称后再决定是否重试。

### 5.4 Scenario (+1) · Delete an idle Session

```mermaid
sequenceDiagram
participant U as User
participant T as TUI adapter
participant C as CLI Runtime client
participant R as Agent Runtime

U->>T: /sessions then Ctrl+D
T->>T: reject current or active target
T->>C: delete_session(id)
C->>R: direct call or one Shared frame
R->>R: existing delete owner
R-->>C: applied / failed / outcome_unknown
C-->>T: typed result
T-->>U: remove only after applied
```

Embedded 和 Shared 最终调用同一 `AgentRuntime::delete_session`。Shared Server 只在请求方没有活动 Turn、目标 Session 未被任何 Client 控制时调用 Runtime owner;`session_in_use` 和 `not_found` 保持结构化错误。TUI 复用现有单个 Session 异步任务槽位,不阻塞事件循环,也不自动重试结果不确定的删除。

## 6. 隔离和生命周期原则

实例身份与 ownership key 分工不同:
Expand Down Expand Up @@ -374,7 +397,7 @@ Session/Turn、事件恢复、Permission/UserInput、Controller、配置管理
|---|---|
| 当前 consumer | 仅第一方交互式 TUI adapter;不自动包含 GUI、Headless CLI、Remote 或 SDK Host |
| 稳定测试合同 | 本机 endpoint、initialize-first、128 KiB request / 8 MiB response-event 上限、连接上限、owner-checked cleanup、原子 Session controller 切换、单连接单活动 Turn、事件流失效后 fail closed、断连取消、30 秒空闲退出 |
| 当前业务范围 | Session/Turn/transcript、当前 Session name/Agent mode/model、Permission/UserInput 的 TUI 必需子集;任何新增操作都需要真实 consumer 和 owner 等价测试 |
| 当前业务范围 | Session list/create/restore/delete、Turn/transcript、当前 Session name/Agent mode/model、Permission/UserInput 的 TUI 必需子集;任何新增操作都需要真实 consumer 和 owner 等价测试 |
| 协议地位 | crate 保持 `publish = false`;这是 workspace 内私有协议,不是 Agent SDK 或远程兼容承诺 |

架构守卫只允许 CLI 消费该 crate;IPC 可以复用稳定的 Event、Product Domain 与 Runtime Port DTO,但禁止依赖 Runtime 实现、SDK Host、services、Tauri 或远程网络 transport。
Expand Down
4 changes: 2 additions & 2 deletions scripts/core-boundaries/rules/source/forbidden-rules.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ export const forbiddenContentRules = [
reason: 'agent-runtime-ipc operation scope is frozen to the reviewed Shared TUI slice',
patterns: [
{
regex: /^\s+(?!(?:Health|ListSessions|CreateSession|RestoreSession|RenameSession|UpdateSessionMode|UpdateSessionModel|ReloadSessionContext|SubmitTurn|CancelTurn|PendingPermissions|RespondPermission|SubmitUserAnswers|Unit|Sessions|SessionCreated|SessionRestored|TurnAccepted|TurnCancelled|Self|AgentContextReloadRequest|AgentDialogTurnRequest|AgentSessionCreateRequest|AgentSessionCreateResult|AgentSessionListRequest|AgentSessionModeUpdateRequest|AgentSessionModelUpdateRequest|AgentSessionSummary|AgentTurnCancellationRequest|AgentTurnCancellationResult|SessionTranscript)\b)[A-Z][A-Za-z0-9_]*\b/,
regex: /^\s+(?!(?:Health|ListSessions|CreateSession|RestoreSession|DeleteSession|RenameSession|UpdateSessionMode|UpdateSessionModel|ReloadSessionContext|SubmitTurn|CancelTurn|PendingPermissions|RespondPermission|SubmitUserAnswers|Unit|Sessions|SessionCreated|SessionRestored|TurnAccepted|TurnCancelled|None|CurrentController|AttachExisting|UncontrolledTarget|Self|RuntimeIpcSessionRequirement|RuntimeIpcOperationRules|AgentContextReloadRequest|AgentDialogTurnRequest|AgentSessionCreateRequest|AgentSessionCreateResult|AgentSessionListRequest|AgentSessionModeUpdateRequest|AgentSessionModelUpdateRequest|AgentSessionSummary|AgentTurnCancellationRequest|AgentTurnCancellationResult|SessionTranscript)\b)[A-Z][A-Za-z0-9_]*\b/,
message:
'agent-runtime-ipc may not add replay, observer, controller-transfer, deletion, fork, or other operations beyond the reviewed Shared TUI slice',
'agent-runtime-ipc may not add archive, replay, observer, controller-transfer, fork, or other operations beyond the reviewed Shared TUI slice',
},
],
},
Expand Down
1 change: 1 addition & 0 deletions scripts/core-boundaries/self-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4858,6 +4858,7 @@ export function runManifestParserSelfTest({
'ManageAgents',
].every((name) => runtimeIpcOperationPattern.test(` ${name},`)) ||
runtimeIpcOperationPattern.test(' Health,') ||
runtimeIpcOperationPattern.test(' DeleteSession {') ||
runtimeIpcOperationPattern.test(' RenameSession {') ||
runtimeIpcOperationPattern.test(' UpdateSessionMode {') ||
runtimeIpcOperationPattern.test(' UpdateSessionModel {') ||
Expand Down
19 changes: 18 additions & 1 deletion src/apps/cli/src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ pub(crate) enum ActionHandler {
pub(crate) const SHARED_TUI_EMBEDDED_HANDOFF: &str =
"Exit all Shared TUI clients, wait up to 30 seconds for their Runtime to stop, then use default Embedded `bitfun chat`";
pub(crate) const SHARED_TUI_HELP_NOTE: &str =
"Shared TUI: start with `bitfun chat --shared`. Multiple TUI processes reuse one workspace Runtime, while each TUI controls at most one Session and each Session has one controller. Use `/rename <name>` to rename the current Session, `/agent`, Tab, or Shift+Tab to change its Agent mode, `/models` to change its model, and `/reload [skills|instructions]` to refresh declarative context for the next message. Model configuration, Agent/Subagent management, MCP, extension, account-sync, usage, and other management remain Embedded. Exit all Shared TUI clients and wait up to 30 seconds before returning to default Embedded `bitfun chat`.";
"Shared TUI: start with `bitfun chat --shared`. Multiple TUI processes reuse one workspace Runtime, while each TUI controls at most one Session and each Session has one controller. Use `/sessions` and Ctrl+D to delete an idle, non-current Session; use `/rename <name>` to rename the current Session, `/agent`, Tab, or Shift+Tab to change its Agent mode, `/models` to change its model, and `/reload [skills|instructions]` to refresh declarative context for the next message. Model configuration, Agent/Subagent management, MCP, extension, account-sync, usage, and other management remain Embedded. Exit all Shared TUI clients and wait up to 30 seconds before returning to default Embedded `bitfun chat`.";

impl ActionHandler {
pub(crate) const fn available_in_shared_tui(self, context: ActionContext) -> bool {
Expand Down Expand Up @@ -1840,10 +1840,27 @@ mod tests {
assert!(SHARED_TUI_HELP_NOTE.contains("`/models`"));
assert!(SHARED_TUI_HELP_NOTE.contains("`/rename <name>`"));
assert!(SHARED_TUI_HELP_NOTE.contains("`/reload [skills|instructions]`"));
assert!(SHARED_TUI_HELP_NOTE.contains("Ctrl+D"));
assert!(SHARED_TUI_HELP_NOTE.contains("idle, non-current Session"));
assert!(SHARED_TUI_HELP_NOTE.contains("Agent/Subagent management"));
assert!(SHARED_TUI_HELP_NOTE.contains("remain Embedded"));
}

#[test]
fn shared_startup_session_list_keeps_the_supported_delete_action() {
let source = include_str!("ui/startup.rs").replace("\r\n", "\n");
let selector = source
.split_once("fn show_session_selector(&mut self)")
.expect("startup session selector")
.1
.split_once("fn show_model_selector(&mut self)")
.expect("startup session selector boundary")
.0;

assert!(selector.contains(".show(session_items, None, true)"));
assert!(!selector.contains("Session deletion is unavailable in Shared TUI"));
}

#[test]
fn rename_is_an_idle_current_session_chat_action() {
let action = action_by_id("rename_session", ActionContext::Chat)
Expand Down
Loading