Book 第二十九章:怎样把这些东西收束成 C# 蓝图
第六部分:迁移与附录

第二十九章:怎样把这些东西收束成 C# 蓝图

把前面的判断收束成 C# 版模块边界、接口草图和实现顺序。

1. 为什么现在该收口到 C# 蓝图

前面 21 篇其实已经把 Claude Code 的主骨架拆得差不多了:

  • turn loop
  • tool runtime
  • 权限
  • agent / skill / task
  • MCP
  • 上下文治理
  • overflow recovery

如果再继续横向拆源码,收益会越来越小。

现在最划算的事,是把这些稳定结论真正翻成:

一份能指导 C# 落地的运行时蓝图。

这篇的目标不是“再解释 Claude Code”,而是回答两个更实际的问题:

  1. 如果用 C# 重做,应该分成哪些模块和接口
  2. 哪些地方该照抄设计,哪些地方该趁迁移顺手做干净

2. 先说总判断

如果只用一句话概括我对 C# 版的建议,那就是:

不要把 Claude Code 翻译成一堆工具类,要把它翻译成一个 agent runtime。

也就是说,C# 版最先该立住的不是:

  • BashTool.cs
  • ReadTool.cs
  • EditTool.cs

而是这几层:

  1. ConversationRuntime
  2. TurnLoop
  3. ToolRuntime
  4. ContextPressurePipeline
  5. ExtensionRuntime
  6. SessionPersistence

工具只是挂在这套 runtime 上的能力面。

3. 我建议的总分层

先给一版总图。

flowchart TB
    Host["CLI / IDE / Desktop / Web Host"] --> Runtime["ConversationRuntime"]
    Runtime --> TurnLoop["TurnLoop"]
    TurnLoop --> Model["IModelGateway"]
    TurnLoop --> Tools["ToolRuntime"]
    TurnLoop --> Context["ContextPressurePipeline"]
    TurnLoop --> Recovery["TurnRecoveryLoop"]
    TurnLoop --> Store["SessionStore"]

    Tools --> Registry["ToolRegistry"]
    Tools --> Scheduler["ToolScheduler"]
    Tools --> Permissions["PermissionEvaluator"]
    Tools --> Hooks["ToolHookPipeline"]

    Context --> Budget["ToolResultBudget"]
    Context --> Snip["Snip / Optional"]
    Context --> Micro["MicroCompactor"]
    Context --> Collapse["ContextCollapseCoordinator"]
    Context --> Compact["ConversationCompactor"]

    Runtime --> Extensions["ExtensionRuntime"]
    Extensions --> MCP["McpConnectionManager"]
    Extensions --> Skills["SkillCatalog"]
    Extensions --> Agents["AgentRuntime"]

    Store --> Transcript["TranscriptStore"]
    Store --> State["RuntimeStateStore"]
    Store --> Files["ArtifactStore"]

这张图背后的核心思想是:

  • 宿主和运行时分离
  • turn loop 和工具运行时分离
  • 工具运行时和上下文治理分离
  • 扩展系统和基础工具系统分离
  • transcript 持久化和内存状态分离

4. TypeScript 到 C# 的一版映射表

Claude Code 现有模块C# 建议落点
query.tsConversationRuntime/TurnLoop/TurnStateMachine
QueryEngine.tsConversationSession
Tool.tsITool + IToolPresentationAdapter + IToolClassifierAdapter
services/tools/*ToolRuntime
services/compact/*ContextPressurePipeline
services/mcp/*McpConnectionManager + McpToolAdapterFactory
skills/*SkillCatalog + SkillLoader
tasks/*TaskBoard + BackgroundTaskRegistry
utils/sessionStorage.tsTranscriptStore
state/AppState*RuntimeStateStore + UiStateStore
components/* / screens/*Host adapters

这张表里最重要的一点是:

不要把 AppState 原样翻成一个 C# 超大对象。

TypeScript 那样写有它的历史包袱,但你重写时没必要一起背过去。

5. 建议的命名空间结构

我建议第一版就把命名空间切干净。

ClaudeCode.Core
ClaudeCode.Runtime
ClaudeCode.Runtime.Turns
ClaudeCode.Runtime.Tools
ClaudeCode.Runtime.Context
ClaudeCode.Runtime.Recovery
ClaudeCode.Runtime.Extensions
ClaudeCode.Runtime.Permissions
ClaudeCode.Runtime.Presentation
ClaudeCode.Persistence
ClaudeCode.Persistence.Transcript
ClaudeCode.Persistence.Artifacts
ClaudeCode.Host.Abstractions
ClaudeCode.Host.Cli
ClaudeCode.Host.Desktop

最少也要守住两条边界:

  • Host.* 不能反向依赖具体 runtime 实现细节
  • Persistence.* 不能知道 UI 宿主长什么样

6. 运行时核心对象,我建议这样拆

6.1 会话壳

public interface IConversationSession
{
    SessionId Id { get; }
    IReadOnlyList<ConversationMessage> Messages { get; }
    Task<TurnResult> SubmitAsync(UserInput input, CancellationToken ct);
}

职责:

  • 持有消息历史
  • 提供高层提交入口
  • 对宿主暴露最小 API

6.2 turn loop

public interface ITurnLoop
{
    IAsyncEnumerable<TurnEvent> RunAsync(
        TurnRequest request,
        CancellationToken ct);
}

职责:

  • 组装本轮上下文
  • 调模型
  • 执行工具
  • 处理 recoverable error
  • 决定继续还是终止

这层就是 query.ts 的核心翻译位。

6.3 turn state

public sealed class TurnLoopState
{
    public List<ConversationMessage> Messages { get; init; } = [];
    public int TurnCount { get; set; }
    public int MaxOutputTokensRecoveryCount { get; set; }
    public bool HasAttemptedReactiveCompact { get; set; }
    public ContinueReason? Transition { get; set; }
    public AutoCompactTrackingState? AutoCompactTracking { get; set; }
}

这层不要怕显式。

Claude Code 的经验已经说明了:

恢复路径一多,显式状态对象比散落局部变量稳得多。

7. 工具运行时,我建议拆成四块

7.1 工具协议

public interface ITool
{
    string Name { get; }
    string Description { get; }
    bool IsConcurrencySafe { get; }
    bool IsReadOnly { get; }

    Task<ToolCallResult> CallAsync(
        ToolCallContext context,
        JsonElement input,
        CancellationToken ct);
}

7.2 工具执行器

public interface IToolExecutor
{
    Task<ToolExecutionResult> ExecuteAsync(
        ToolUseRequest request,
        CancellationToken ct);
}

职责:

  • schema 解析
  • backfill observable input
  • pre / post hooks
  • permission check
  • tool.call
  • tool_result block 映射

7.3 工具调度器

public interface IToolScheduler
{
    Task<ToolBatchResult> RunBatchAsync(
        IReadOnlyList<ToolUseRequest> requests,
        CancellationToken ct);
}

职责:

  • 串行 / 并行调度
  • sibling cancel
  • 流式结果稳定输出顺序

7.4 权限评估器

public interface IPermissionEvaluator
{
    Task<PermissionDecision> EvaluateAsync(
        ToolPermissionRequest request,
        CancellationToken ct);
}

这一层一定要保留三态:

  • Allow
  • Ask
  • Deny

不要偷懒退化成 bool

8. 上下文治理层,我建议拆成 pipeline

8.1 总接口

public interface IContextPressurePipeline
{
    Task<ContextPreparationResult> PrepareAsync(
        ContextPreparationRequest request,
        CancellationToken ct);
}

8.2 内部阶段

public interface IToolResultBudgetEnforcer { ... }
public interface ISnipCoordinator { ... }
public interface IMicroCompactor { ... }
public interface IContextCollapseCoordinator { ... }
public interface IConversationCompactor { ... }
public interface IPostCompactRestorer { ... }

最重要的不是名字,而是:

不要把所有上下文处理都塞进一个 CompactAsync()

Claude Code 已经证明这会长成多层治理链,而不是一个摘要函数。

9. 恢复层要独立,不要混进 compact

这块我建议单独拉出来。

public interface ITurnRecoveryLoop
{
    Task<RecoveryDecision> TryRecoverAsync(
        RecoveryContext context,
        AssistantMessage errorMessage,
        CancellationToken ct);
}

里面至少区分三类恢复:

  • PromptTooLong
  • MediaTooLarge
  • MaxOutputTokens

以及几类动作:

  • CollapseDrainRetry
  • ReactiveCompactRetry
  • EscalateMaxOutputTokens
  • ContinuationRetry
  • Fail

这里千万不要直接写成:

if error then compact

那样会把 Claude Code 最值钱的恢复策略全抹平。

10. 扩展系统,我建议拆成 runtime,不要拆成插件点杂烩

10.1 MCP

public interface IMcpConnectionManager
{
    Task<IReadOnlyList<IMcpClient>> GetConnectedClientsAsync(CancellationToken ct);
}

public interface IMcpToolAdapterFactory
{
    IReadOnlyList<ITool> CreateTools(IMcpClient client);
}

10.2 技能

public interface ISkillCatalog
{
    Task<IReadOnlyList<SkillDefinition>> ListAsync(CancellationToken ct);
    Task<SkillPayload> LoadAsync(SkillId id, CancellationToken ct);
}

10.3 agent / subagent

public interface IAgentRuntime
{
    Task<AgentSpawnResult> SpawnAsync(AgentSpawnRequest request, CancellationToken ct);
    Task<AgentResumeResult> ResumeAsync(AgentId agentId, CancellationToken ct);
}

不要把这三类能力塞进一个 ExtensionManager 就结束了。

Claude Code 的经验已经很明确:

  • MCP 是动态工具工厂
  • Skill 是上下文修改器
  • Agent 是执行主体工厂

这三者不是一回事。

11. 持久化层一定要分 transcript 和 artifact

11.1 transcript store

public interface ITranscriptStore
{
    Task AppendAsync(TranscriptEntry entry, CancellationToken ct);
    Task<ResumeSnapshot> LoadAsync(SessionId sessionId, CancellationToken ct);
}

职责:

  • 消息
  • content replacements
  • context collapse commits / snapshots
  • metadata

11.2 artifact store

public interface IArtifactStore
{
    Task<StoredArtifact> SaveToolResultAsync(
        ToolResultArtifact artifact,
        CancellationToken ct);
}

职责:

  • 大输出文件
  • 中间产物
  • 临时文件引用

这两层不要混。

Claude Code 里已经很清楚:

  • transcript 是会话事实
  • artifact 是大内容旁路存储

12. 宿主层要尽量薄

如果以后你想同时支持:

  • 终端 CLI
  • 桌面 App
  • IDE 插件
  • HTTP API

那宿主层最好只做三件事:

  1. 收用户输入
  2. 订阅 TurnEvent
  3. 处理 Ask / Notify / Render

我建议用一个统一的宿主桥接口:

public interface IHostBridge
{
    Task PublishAsync(TurnEvent evt, CancellationToken ct);
    Task<UserDecision> AskAsync(AskRequest request, CancellationToken ct);
}

这样 CLI、桌面、Web 都能挂同一套 runtime。

13. 一版类图

classDiagram
    class ConversationSession {
      +SessionId Id
      +SubmitAsync(input, ct)
    }

    class TurnLoop {
      +RunAsync(request, ct)
    }

    class ToolRuntime {
      +ExecuteAsync(request, ct)
    }

    class ToolScheduler {
      +RunBatchAsync(requests, ct)
    }

    class PermissionEvaluator {
      +EvaluateAsync(request, ct)
    }

    class ContextPressurePipeline {
      +PrepareAsync(request, ct)
    }

    class TurnRecoveryLoop {
      +TryRecoverAsync(context, error, ct)
    }

    class McpConnectionManager
    class SkillCatalog
    class AgentRuntime
    class TranscriptStore
    class ArtifactStore
    class HostBridge

    ConversationSession --> TurnLoop
    TurnLoop --> ToolRuntime
    TurnLoop --> ContextPressurePipeline
    TurnLoop --> TurnRecoveryLoop
    TurnLoop --> TranscriptStore
    TurnLoop --> HostBridge

    ToolRuntime --> ToolScheduler
    ToolRuntime --> PermissionEvaluator
    ContextPressurePipeline --> ArtifactStore
    TurnLoop --> McpConnectionManager
    TurnLoop --> SkillCatalog
    TurnLoop --> AgentRuntime

14. 哪些地方建议照抄,哪些地方别照抄

14.1 建议照抄

  • turn loop 用显式状态对象
  • 权限三态
  • 工具执行前后的 hook 生命周期
  • 工具结果“给模型”和“给用户”的双视图
  • content replacement / collapse / compact 分层治理
  • resume 时恢复运行时治理状态

14.2 不建议照抄

  • AppState 式超大对象
  • UI 渲染方法直接塞进核心 ITool
  • 大量模块级可变单例状态
  • 宿主和 runtime 强绑定
  • feature flag 分支散在所有类里

TypeScript 版这么长出来,有它的工程历史;
但 C# 重写最大的优势,就是你可以顺手把边界做干净。

15. 我建议的实现顺序

Phase 1:先立内核,不做花活

先做:

  • ConversationSession
  • TurnLoop
  • IModelGateway
  • ToolRegistry
  • ToolExecutor
  • PermissionEvaluator
  • TranscriptStore

这时先只支持:

  • 普通 assistant turn
  • 串行工具调用
  • 基础 resume

Phase 2:补本地工作能力

再做:

  • FileRead
  • FileEdit
  • FileWrite
  • Glob
  • Grep
  • Bash

这是最小可用 coding runtime。

Phase 3:补上下文治理

再补:

  • tool result persistence
  • aggregate budget
  • microcompact
  • full compact
  • overflow recovery

这一步做完,长会话才真的能跑。

Phase 4:补扩展系统

再上:

  • MCP
  • skills
  • subagent
  • tasks

Phase 5:最后再做宿主增强

比如:

  • 桌面 UI
  • richer transcript
  • 可视化 /context
  • REPL wrapper

16. 第一版最小可用目标

如果你的目标不是“先写一堆文档”,而是尽快做出第一个能跑的 C# 版本,我建议最小目标定成这样:

单会话
+ 单模型
+ 串行工具
+ Read/Edit/Write/Bash
+ resume
+ tool-result persistence
+ full compact

先别急着上:

  • MCP
  • skill
  • swarm
  • context collapse
  • REPLTool

因为这些都建立在内核先稳住的前提上。

17. 一句话总结

如果说前面 21 篇是在回答“Claude Code 是怎么设计出来的”,那这篇的答案就是:

C# 版最该先实现的,不是一组工具,而是一套能驱动工具、治理上下文、恢复错误、承载扩展的会话运行时。