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 分钟前
海外恶意 UDP 攻击溯源:分层封禁策略与业务兼容平衡方案
网络·网络协议·udp
笨蛋©19 分钟前
[实战] 制造业数字化:CAD图纸气泡图自动化标注与检验计划生成指南
ai·数字化·cad·质量管理·制造业
iwhitney1 小时前
【次方量化】3分钟搞懂什么是量化策略
python
高洁011 小时前
大模型部署资源不足?轻量化部署解决方案
python·深度学习·机器学习·数据挖掘·transformer
阿里云大数据AI技术1 小时前
MaxFrame 视频帧智能分析:从视频到语义向量的端到端分布式处理
人工智能·python
TimeAground1 小时前
HTTP 协议全解:从报文到 HTTP/3,Android 开发者需要知道的一切
http
淘矿人1 小时前
从0到1:用Claude启动你的第一个项目
开发语言·人工智能·git·python·github·php·pygame
嘻嘻哈哈樱桃2 小时前
牛客经典101题题解集--动态规划
java·数据结构·python·算法·职场和发展·动态规划
gmaajt2 小时前
Golang怎么做国际化多语言_Golang i18n教程【核心】
jvm·数据库·python