claw-code 源码分析:从 REPL 到服务端——CLI / HTTP(SSE) / LSP 多入口如何共享同一颗 runtime 心?

分析对象 :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),它提供:

  • 会话语义SessionConversationMessageContentBlockMessageRole(可持久化,可回放)
  • 会话循环ConversationRuntime(把模型流式事件、工具执行、权限、hooks 缝成 turn loop)
  • 提示构造SystemPromptBuilderProjectContext(把工作区信息变成 system prompt)
  • 压缩/续写compact_session + CompactionConfig(把旧历史折叠成 system continuation)
  • 权限/策略PermissionPolicy 等(决定工具是否允许执行)
  • MCP 子系统 :命名空间、transport bootstrap、JSON-RPC 协议对象(见 result/20.md
  • LSP 能力 :runtime 甚至 re-export lsp crate 的 LspManager 等类型(把"上下文增强"纳入运行时工具箱)

这与 Python 移植层的"shim+报告"不同:Rust runtime 是"要被多个入口装配并实际运行"的内核。


2. 入口层应该做什么:I/O、交互、协议适配

把系统按职责拆开,才能让 CLI / HTTP / LSP 共享同一颗心:

  • runtime 层(共享):纯业务语义与状态机(会话、权限、压缩、prompt build、工具执行抽象)
  • 入口层(各自):输入输出、用户交互、协议封装、错误呈现、进程模型

在代码里,这个分界非常清晰:

  • claw-cli 依赖 runtimeapitoolscommandsplugins(装配"本地交互式产品面")。
  • server 依赖 runtimeaxum(把会话暴露为 HTTP/SSE)。
  • lsp crate 本身是独立库,但 runtime 又把它 re-export 成运行时能力的一部分。

3. REPL/CLI:claw-cli 如何装配 runtime 心

3.1 依赖面:入口层把"外设"拼起来

claw-cliCargo.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.mdSession::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::StreamEventruntime::AssistantEvent、以及 app.rs 中的 StreamEvent),属于产品快速演进期常见现象。成熟阶段通常会收敛为:runtime loop 的统一事件类型,入口层只做渲染。


4. HTTP(SSE):server 如何复用 runtime 的会话模型

rust/crates/server 采用 axum,提供 session CRUD 与 SSE 事件流。它复用的是 runtime 的会话数据结构SessionConversationMessage),并把这些结构序列化为 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.mdruntime/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::StreamEvent vs runtime::AssistantEvent vs 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 变成不同外壳预留了空间。

相关推荐
不解不惑2 小时前
gemma4 实现ASR语音识别
人工智能·python·语音识别
来自远方的老作者2 小时前
第8章 流程控制-8.2 选择结构
开发语言·python·选择结构
kaico20182 小时前
python常用标准库
开发语言·python
TTGGGFF2 小时前
SnapTranslate 2.0:轻量级全场景划词翻译——添加生词本以及生词本复习AI助手功能!
python·划词翻译·git开源
链诸葛2 小时前
Claude Code 推荐指南(一):安装、CLI使用、VSCode 集成
ide·vscode·ai·编辑器·claude
杜子不疼.2 小时前
Python + Selenium + AI 智能爬虫:自动识别反爬与数据提取
人工智能·python·selenium
Elastic 中国社区官方博客2 小时前
Elasticsearch:语义搜索,现在默认支持多语言
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索
财经资讯数据_灵砚智能2 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(日间)2026年4月8日
大数据·人工智能·python·信息可视化·自然语言处理
ly甲烷2 小时前
智能体Skills详细介绍与上手指南
ai·agent·skills