第二十九章:怎样把这些东西收束成 C# 蓝图
把前面的判断收束成 C# 版模块边界、接口草图和实现顺序。
1. 为什么现在该收口到 C# 蓝图
前面 21 篇其实已经把 Claude Code 的主骨架拆得差不多了:
- turn loop
- tool runtime
- 权限
- agent / skill / task
- MCP
- 上下文治理
- overflow recovery
如果再继续横向拆源码,收益会越来越小。
现在最划算的事,是把这些稳定结论真正翻成:
一份能指导 C# 落地的运行时蓝图。
这篇的目标不是“再解释 Claude Code”,而是回答两个更实际的问题:
- 如果用 C# 重做,应该分成哪些模块和接口
- 哪些地方该照抄设计,哪些地方该趁迁移顺手做干净
2. 先说总判断
如果只用一句话概括我对 C# 版的建议,那就是:
不要把 Claude Code 翻译成一堆工具类,要把它翻译成一个 agent runtime。
也就是说,C# 版最先该立住的不是:
BashTool.csReadTool.csEditTool.cs
而是这几层:
ConversationRuntimeTurnLoopToolRuntimeContextPressurePipelineExtensionRuntimeSessionPersistence
工具只是挂在这套 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.ts | ConversationRuntime/TurnLoop/TurnStateMachine |
QueryEngine.ts | ConversationSession |
Tool.ts | ITool + IToolPresentationAdapter + IToolClassifierAdapter |
services/tools/* | ToolRuntime |
services/compact/* | ContextPressurePipeline |
services/mcp/* | McpConnectionManager + McpToolAdapterFactory |
skills/* | SkillCatalog + SkillLoader |
tasks/* | TaskBoard + BackgroundTaskRegistry |
utils/sessionStorage.ts | TranscriptStore |
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);
}
这一层一定要保留三态:
AllowAskDeny
不要偷懒退化成 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);
}
里面至少区分三类恢复:
PromptTooLongMediaTooLargeMaxOutputTokens
以及几类动作:
CollapseDrainRetryReactiveCompactRetryEscalateMaxOutputTokensContinuationRetryFail
这里千万不要直接写成:
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
那宿主层最好只做三件事:
- 收用户输入
- 订阅
TurnEvent - 处理
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:先立内核,不做花活
先做:
ConversationSessionTurnLoopIModelGatewayToolRegistryToolExecutorPermissionEvaluatorTranscriptStore
这时先只支持:
- 普通 assistant turn
- 串行工具调用
- 基础 resume
Phase 2:补本地工作能力
再做:
FileReadFileEditFileWriteGlobGrepBash
这是最小可用 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# 版最该先实现的,不是一组工具,而是一套能驱动工具、治理上下文、恢复错误、承载扩展的会话运行时。