分析对象 :Rust 侧
rust/crates/claw-cli(REPL/CLI)、rust/crates/server(HTTP+SSE)、rust/crates/lsp(LSP 管理与类型)、以及它们与rust/crates/runtime的耦合点。
核心问题:多入口不是"复制一份业务逻辑",而是"共享同一套运行时语义(会话/消息/权限/压缩/提示构造等),在入口层各自做 I/O 与呈现适配"。
1. 先定义"runtime 心"是什么
在本仓库 Rust 侧,"runtime 心"更像一个 可复用的库内核 (runtime crate),它提供:
- 会话语义 :
Session、ConversationMessage、ContentBlock、MessageRole(可持久化,可回放) - 会话循环 :
ConversationRuntime(把模型流式事件、工具执行、权限、hooks 缝成 turn loop) - 提示构造 :
SystemPromptBuilder、ProjectContext(把工作区信息变成 system prompt) - 压缩/续写 :
compact_session+CompactionConfig(把旧历史折叠成 system continuation) - 权限/策略 :
PermissionPolicy等(决定工具是否允许执行) - MCP 子系统 :命名空间、transport bootstrap、JSON-RPC 协议对象(见
result/20.md) - LSP 能力 :runtime 甚至 re-export
lspcrate 的LspManager等类型(把"上下文增强"纳入运行时工具箱)
这与 Python 移植层的"shim+报告"不同:Rust runtime 是"要被多个入口装配并实际运行"的内核。
2. 入口层应该做什么:I/O、交互、协议适配
把系统按职责拆开,才能让 CLI / HTTP / LSP 共享同一颗心:
- runtime 层(共享):纯业务语义与状态机(会话、权限、压缩、prompt build、工具执行抽象)
- 入口层(各自):输入输出、用户交互、协议封装、错误呈现、进程模型
在代码里,这个分界非常清晰:
claw-cli依赖runtime、api、tools、commands、plugins(装配"本地交互式产品面")。server依赖runtime与axum(把会话暴露为 HTTP/SSE)。lspcrate 本身是独立库,但runtime又把它 re-export 成运行时能力的一部分。
3. REPL/CLI:claw-cli 如何装配 runtime 心
3.1 依赖面:入口层把"外设"拼起来
claw-cli 的 Cargo.toml 明确它是一个二进制入口,依赖 runtime 与多个"外设 crate":
12:25:rust/crates/claw-cli/Cargo.toml
[dependencies]
api = { path = "../api" }
commands = { path = "../commands" }
compat-harness = { path = "../compat-harness" }
runtime = { path = "../runtime" }
plugins = { path = "../plugins" }
tools = { path = "../tools" }
...
学习点:CLI 不应该自己实现会话/权限/压缩;它只负责把这些库拼成"可用的交互体验"。
3.2 行为入口:解析参数 → 选择模式 → 调用同一套运行时能力
claw-cli/src/main.rs 将用户行为拆成 CliAction(prompt、repl、resume、login 等),并在 run() 中分发:
rust
// 82:113:rust/crates/claw-cli/src/main.rs
match parse_args(&args)? {
CliAction::Prompt { ... } => LiveCli::new(... )?.run_turn_with_output(&prompt, output_format)?,
CliAction::Repl { ... } => run_repl(model, allowed_tools, permission_mode)?,
CliAction::PrintSystemPrompt { ... } => print_system_prompt(cwd, date),
CliAction::ResumeSession { ... } => resume_session(&session_path, &commands),
CliAction::Login => run_login()?,
...
}
这里的关键是:入口模式不同,但底层"会话/提示/权限/工具/压缩"的抽象可以共用。例如:
PrintSystemPrompt→ 直接调用 runtime 的load_system_prompt/SystemPromptBuilder(提示构造能力被复用)Prompt/Repl→ 通过 "LiveCli/会话客户端" 执行 turn loop(复用会话语义与 stream event)ResumeSession→ 复用 runtime 的 session 持久化格式(见result/20.md的Session::load_from_path)
3.3 REPL 的"交互适配"与运行时事件
claw-cli/src/app.rs 负责 REPL 交互:读输入、识别 /help /compact 等 slash command、渲染 streaming 事件(TextDelta、ToolCallStart/Result、Usage 等)。这部分是典型"入口层工作":把运行时事件翻译成终端 UX。
例如对事件的匹配(节选):
rust
// 216:259:rust/crates/claw-cli/src/app.rs
match event {
StreamEvent::TextDelta(delta) => { ... write!(out, \"{delta}\"); ... }
StreamEvent::ToolCallStart { name, input } => { ... }
StreamEvent::ToolCallResult { name, output, is_error } => { ... }
StreamEvent::Usage(usage) => { *turn_usage = usage; }
...
}
学习点:入口层可以很复杂(渲染、spinner、键盘交互),但它不应该重新定义"什么是 ToolCallResult";事件语义应来自共享内核。
备注:
claw-cli中存在多个"事件类型"来源(api::StreamEvent、runtime::AssistantEvent、以及 app.rs 中的StreamEvent),属于产品快速演进期常见现象。成熟阶段通常会收敛为:runtime loop 的统一事件类型,入口层只做渲染。
4. HTTP(SSE):server 如何复用 runtime 的会话模型
rust/crates/server 采用 axum,提供 session CRUD 与 SSE 事件流。它复用的是 runtime 的会话数据结构 (Session 与 ConversationMessage),并把这些结构序列化为 JSON 发给客户端。
4.1 服务端状态:内存 SessionStore + 广播事件
rust
// 18:66:rust/crates/server/src/lib.rs
pub type SessionId = String;
pub type SessionStore = Arc<RwLock<HashMap<SessionId, Session>>>;
pub struct Session {
pub id: SessionId,
pub created_at: u64,
pub conversation: RuntimeSession,
events: broadcast::Sender<SessionEvent>,
}
...
conversation: RuntimeSession::new(),
这里的 conversation 就是 runtime 的 Session(被 import 为 RuntimeSession),意味着服务端对会话的"真相表示"与 CLI/本地运行时一致。
4.2 SSE 事件:Snapshot + Message(可回放的协议形状)
rust
// 74:85:rust/crates/server/src/lib.rs
#[serde(tag = "type", rename_all = "snake_case")]
enum SessionEvent {
Snapshot { session_id: SessionId, session: RuntimeSession },
Message { session_id: SessionId, message: ConversationMessage },
}
并将事件转成 SSE:
rust
// 95:100:rust/crates/server/src/lib.rs
fn to_sse_event(&self) -> Result<Event, serde_json::Error> {
Ok(Event::default().event(self.event_name()).data(serde_json::to_string(self)?))
}
学习点:HTTP/SSE 入口层要解决的是"怎么把会话变化推送给外部客户端",而不是"会话怎么表示"。这就是复用 runtime 心带来的直接收益:客户端拿到的 JSON 结构与本地持久化/回放可以对齐。
当前 server 的
send_message只把 user message 追加进 session,并未运行完整 ConversationRuntime(即未调用模型、工具、权限、hooks)。这符合"先把 transport 与会话协议跑通"的演进节奏;未来要让 server 成为真正 agent backend,只需在send_message里注入 ConversationRuntime 的 turn loop,并把 assistant/tool 产生的消息也 broadcast 出去。
5. LSP:把"编辑器语义"作为可注入的上下文增强
lsp crate 本身提供 LspManager、diagnostics、definition/references 等能力;而 runtime crate 在 lib.rs 里直接 pub use lsp::{ ... },相当于把 LSP 视为 runtime 的"可选能力模块"。(见 result/20.md 的 runtime/src/lib.rs re-export。)
prompt.rs 里也能看到 SystemPromptBuilder.with_lsp_context(&LspContextEnrichment):
rust
// 134:141:rust/crates/runtime/src/prompt.rs
pub fn with_lsp_context(mut self, enrichment: &LspContextEnrichment) -> Self {
if !enrichment.is_empty() {
self.append_sections.push(enrichment.render_prompt_section());
}
self
}
学习点 :LSP 在这里不是"另一个入口",更像 runtime 心的"输入增强器":
CLI、server、甚至未来的 IDE 端都可以把 LSP 汇总出的上下文注入到 prompt 构造中,从而共享同一套推理输入格式。
6. 多入口共享 runtime 心的"成功条件"与"常见裂缝"
6.1 成功条件(建议对齐的最小契约)
- 统一会话结构 :
Session/ConversationMessage/ContentBlock是跨入口的共享真相。 - 统一事件语义:streaming 事件(文本增量、工具调用、usage、stop)应有单一来源(理想是 runtime)。
- 统一权限与拒绝落点 :deny 应进入会话消息或事件流(Rust runtime 已把 hook deny 写成 tool_result,见
result/20.md)。 - 统一持久化格式与版本:server 的 snapshot、CLI 的 session file、export/diff 应对齐 schema/version。
6.2 常见裂缝(本仓库也能看到的演进信号)
- 事件类型分裂 :
api::StreamEventvsruntime::AssistantEventvs CLI 自己的StreamEvent。短期可用,长期会带来入口间不一致。 - server 只做会话存储,不跑 turn loop:这是一种合理的渐进,但要明确"它目前只是 transport + store"。
- 配置/权限策略没贯穿所有入口 :如果 CLI 有权限模式,server 也需要同样的权限模型,否则行为会漂移(见
result/19.md的双轨漂移风险)。
7. 小结
从 REPL 到服务端再到 LSP,多入口共享同一颗 runtime 心的关键是:
runtime 提供可复用的会话/权限/压缩/提示构造/工具抽象;入口层只负责 I/O 与协议/交互适配。
本仓库 Rust 侧已经具备这种形态:
- CLI 作为"交互装配器"
- server 作为"协议与事件分发器"
- LSP 作为"上下文增强能力"
共同复用 runtime 的类型与语义基座,为后续把 server 变成真正 agent backend、把 CLI/IDE 变成不同外壳预留了空间。