涉及源码 :Python
src/models.py、commands.py、tools.py、query_engine.py;Rustruntime(conversation.rs、session.rs、permissions.rs)、api(流式类型)、tools(execute_tool)。
1. TS 心智在移植里的「锚」是什么?
TypeScript 习惯里常见三层叠在一起:
- JSON / API 线 :discriminated union(
type字段)、可选字段、与后端 schema 对齐。 - 领域模型:不可变 props、明确 null/undefined 语义。
- 错误模型 :
Result模式(neverthrow 等)或 抛异常 +try/catch,或 HTTP 状态码与 body 分流。
claw-code 没有 在 Python 里复刻完整 TS 类型系统,而是用 三条可对照的轴线 做跨栈对齐:
| 轴线 | Python port 侧重 | Rust 产品侧重 |
|---|---|---|
| 边界载荷 | reference_data/*.json → dataclass / 元组 |
serde + JSON Schema 式 ToolSpec、Provider StreamEvent |
| 会话/消息形状 | 弱:用户串、TurnResult 文本 |
强:ContentBlock 标签枚举、MessageRole、Session 序列化 |
| 错误与拒绝 | handled 标志 + 字符串 message;少量 Exception |
Result<_, E>、PermissionOutcome、ToolError / String |
下面分维度说明「怎么对齐」以及 刻意不对齐 之处(port vs product)。
2. 类型:从「快照 schema」到语言内类型
2.1 Python:dataclass 承载清单元数据,JSON 为真源
命令/工具清单从快照反序列化进 不可变 PortingModule,保证与 TS 侧「条目列表」同构的是 数据,不是行为:
python
# 22:31:src/commands.py
PortingModule(
name=entry['name'],
responsibility=entry['responsibility'],
source_hint=entry['source_hint'],
status='mirrored',
)
from __future__ import annotations + PortingModule | None 等,对应 TS 的 可选与联合 的轻量子集:运行时仍靠约定 ,靠测试与 CLI 固定输出守住契约(result/26.md)。
2.2 Rust:serde 标签枚举对齐 API/会话 wire shape
会话消息块用 #[serde(tag = "type")] 模拟 TS 式 discriminated union,边界清晰:
rust
// 20:37:rust/crates/runtime/src/session.rs
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentBlock {
Text {
text: String,
},
ToolUse {
id: String,
name: String,
input: String,
},
ToolResult {
tool_use_id: String,
tool_name: String,
output: String,
is_error: bool,
},
}
ToolResult 的 is_error: bool 对应「工具失败不进异常通道、而进对话历史」的常见 harness 语义,与 TS 里「结果对象 + error 标志」心智一致。
2.3 Usage:同一概念,不同严格度
- Rust:
TokenUsage用u32+serde,与 API 用量字段同形(result/24.md)。 - Python:
UsageSummary用int,且add_turn用 词数近似 token------刻意不假装与提供商 usage 一致:
python
# 28:36:src/models.py
@dataclass(frozen=True)
class UsageSummary:
input_tokens: int = 0
output_tokens: int = 0
def add_turn(self, prompt: str, output: str) -> 'UsageSummary':
return UsageSummary(
input_tokens=self.input_tokens + len(prompt.split()),
output_tokens=self.output_tokens + len(output.split()),
)
对齐策略 :名字对齐(便于读源码的人迁移心智),语义在文档与测试里降级说明,避免类型同名却误用。
3. 边界:哪里是「契约面」,哪里是「演示面」
3.1 Python CommandExecution / ToolExecution:handled 而非 Result
执行 API 总返回 结构体,用 handled 区分成功解析镜像条目 vs 未知名------接近「永远有值」的 TS 返回类型,而 不是 Rust 的 Result:
python
# 75:80:src/commands.py
def execute_command(name: str, prompt: str = '') -> CommandExecution:
module = get_command(name)
if module is None:
return CommandExecution(name=name, source_hint='', prompt=prompt, handled=False, message=f'Unknown mirrored command: {name}')
action = f"Mirrored command '{module.name}' from {module.source_hint} would handle prompt {prompt!r}."
return CommandExecution(name=module.name, source_hint=module.source_hint, prompt=prompt, handled=True, message=action)
python
# 81:86:src/tools.py
def execute_tool(name: str, payload: str = '') -> ToolExecution:
module = get_tool(name)
if module is None:
return ToolExecution(name=name, source_hint='', payload=payload, handled=False, message=f'Unknown mirrored tool: {name}')
action = f"Mirrored tool '{module.name}' from {module.source_hint} would handle payload {payload!r}."
return ToolExecution(name=module.name, source_hint=module.source_hint, payload=payload, handled=True, message=action)
与 Rust 对照 :Rust runtime 的 ToolExecutor 是 Result<String, ToolError> ,失败是 类型系统的一轨:
rust
// 35:37:rust/crates/runtime/src/conversation.rs
pub trait ToolExecutor {
fn execute(&mut self, tool_name: &str, input: &str) -> Result<String, ToolError>;
}
移植教益 :Port 层用 handled + message 适合 CLI 演示与快照回归 ;产品层用 Result 适合 组合与强制处理 。混用时要在架构上画线(result/24.md 已强调 Python 非企业主路径)。
3.2 Rust 工具分发:Result<String, String> 与 serde_json::Value
execute_tool 入口是 serde_json::Value → from_value 成强类型 input → 再 to_string_pretty 成工具输出字符串;错误统一 String(含反序列化失败与业务 Err):
rust
// 539:567:rust/crates/tools/src/lib.rs
pub fn execute_tool(name: &str, input: &Value) -> Result<String, String> {
match name {
"bash" => from_value::<BashCommandInput>(input).and_then(run_bash),
...
_ => Err(format!("unsupported tool: {name}")),
}
}
fn from_value<T: for<'de> Deserialize<'de>>(input: &Value) -> Result<T, String> {
serde_json::from_value(input.clone()).map_err(|error| error.to_string())
}
这与 TS「工具输入是 JSON,错误变成可展示字符串」的心智接近;未 在整个 workspace 强行统一 thiserror 枚举,属于 工程折中。
4. 错误模型:异常、枚举与 Result 的分工
4.1 对话运行时:RuntimeError 与流式解析
ApiClient::stream → Result<Vec<AssistantEvent>, RuntimeError>;run_turn 同样 Result<TurnSummary, RuntimeError> 。单层字符串错误 包装,便于从 TS「throw Error(message)」迁思维,但 丢失错误分类 (若需重试用 anyhow/枚举需后续演进)。
4.2 会话持久化:枚举 SessionError + From 转换
与「一切皆 String」相比,Session IO/JSON 用 可区分变体:
rust
// 52:80:rust/crates/runtime/src/session.rs
#[derive(Debug)]
pub enum SessionError {
Io(std::io::Error),
Json(JsonError),
Format(String),
}
...
impl From<std::io::Error> for SessionError {
fn from(value: std::io::Error) -> Self {
Self::Io(value)
}
}
impl From<JsonError> for SessionError {
fn from(value: JsonError) -> Self {
Self::Json(value)
}
}
对应 TS 里 区分 fs 失败 vs parse 失败 的 instanceof / code 检查,Rust 用 枚举 + trait 显式化。
4.3 权限:与错误通道分离的 PermissionOutcome
授权结果 不是 ToolError,而是独立枚举,便于在 match 里先分流再执行工具------对齐「权限拒绝 ≠ 工具执行异常」的 harness 语义:
rust
// 43:47:rust/crates/runtime/src/permissions.rs
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PermissionOutcome {
Allow,
Deny { reason: String },
}
Python 侧则用 ToolPermissionContext 过滤列表 + PermissionDenial 出现在 TurnResult(query_engine),是 另一条、更偏演示的建模。
4.4 Python 中真正的异常路径
例如结构化输出渲染失败 RuntimeError (result/25.md)------与「正常 turn 总返回 TurnResult」不同,属于 编码/序列化层 的硬失败。
5. 小结:对齐 checklist(给跨栈移植用)
- 先锁定 wire format :API JSON、会话 JSON、工具 input schema------用
serde/ 快照 JSON 做 单一真源,再在各自语言里生成类型(Rust derive;Python dataclass 手写或 codegen)。 - 错误三轨 :解析错误 、业务拒绝(权限) 、运行时失败(工具/API) ------TS 里常混在 Promise reject;Rust 已部分拆开(
PermissionOutcomevsToolError);Python port 用handled/tuple 简化。 - 不要同名不同义 :
UsageSummaryvsTokenUsage是正面例子------名称继承心智,实现诚实降级。 - 边界与核心分离 :镜像
execute_*≠ 真实ConversationRuntime;文档与测试要写清,否则类型再漂亮也会误用。