架构总览
Tether 是 deepseek-harness(dsh)的 C# 重实现, 建立在 Cordis 插件框架之上——一切皆插件。本页说明包为什么这么划分,以及划分背后的三条规则。
两层
分层是硬约束,不是风格偏好。
Cordis.*—— 与 agent 无关的通用插件框架层。它不知道什么是 LLM、什么是工具调用。Tether.*—— agent harness 产品层。能力按 Service Definition、Provider、Consumer 三种角色组合,进程入口只负责装配插件树。
Tether 以 dsh 的调用方可观察语义为复刻目标,但允许采用符合 .NET 生态的包划分、类型和内部实现。 复刻基线与对齐制度见 dsh 基线与对齐。
三条结构性规则
一、Cordis 与 Tether 分层。 Cordis 是通用插件框架,保持独立;Tether 建立在它之上。框架核心禁止引用任何 Tether.* 包。
二、能力 seam 决定是否拆程序集。 Service Definition 单独一个程序集,里面只有接口与数据类型;Provider 与 Consumer 都引用它。这样组合预设时只换实现、不换引用。
三、Composition Root 收敛到 Boot。 进程入口只管日志与宿主,插件树由加载器从 YAML 挂载,保持一切可 patch。
实际的依赖图
下表来自各 csproj 的 ProjectReference,不是设计意图的描述。
零引用的定义程序集
这些程序集一个 ProjectReference 都没有——连 Cordis 都不引用。
| 程序集 | 定义什么 |
|---|---|
Cordis | 插件框架核心 |
Tether.Core.Contracts | 会话、Agent、工具、提示词、审批的契约 |
Tether.Llm | 模型访问 seam |
Tether.Fs | 存储原语 seam |
Tether.Shell | shell 执行 seam(前台 + 后台 job) |
Tether.Subprocess | 进程原语 seam |
第一条规则因此是被机械保证的,不依赖人的自觉;第二条规则也在这里落地:换实现永远不动引用。
能力包的三角色模式
每块能力都按同一个形状展开,引用关系是可预测的公式:
| 角色 | 命名 | 引用公式 |
|---|---|---|
| seam | Tether.X | 无 |
| Provider | Tether.X.Local | Cordis + Tether.Invariants + Tether.X |
| 工具 | Tether.X.Tools | Cordis + Tether.Core.Contracts + Tether.Invariants + Tether.X |
.Local 换成别的 Provider 时,工具与消费者的引用一行都不用改。按这个模式展开的能力:
| 能力 | seam | Provider | 工具 |
|---|---|---|---|
| 子进程 | Tether.Subprocess | .Local | — |
| Shell | Tether.Shell | .Local(bash / pwsh) | .Tools |
| 文件系统 | Tether.Fs | .Local | .Tools、.Search |
| 模型访问 | Tether.Llm | .DeepSeek、.Providers | — |
| 后台作业与持久终端 | Tether.Jobs、Tether.Terminal | .Local | .Tools |
| 沙箱 | Tether.Sandbox | .Local | — |
| LSP | Tether.Lsp | .Stdio | .Tools |
| Web 访问 | Tether.Web | .Http | .Tools |
| 设置 | Tether.Settings | .Local | — |
| 凭据 | Tether.Credentials | .Local | — |
| 附件与溢写 | Tether.Attachment、Tether.Spill | .Local | — |
| 会话持久化 | Tether.Session.Persistence | .Jsonl、.Sqlite | — |
同一个形状重复了十余次,这不是巧合——它是第二条规则被机械执行的结果。看到一个新的 Tether.X 目录,不用读代码就能推断出它是零引用的接口包,Tether.X.Local 是它的本地实现,Tether.X.Tools 把它暴露给模型。
会话持久化在三角色之外再显式拆出 .Coordinator:它消费唯一的 ISessionEventStore Provider,并向其它 consumers 提供 ISessionPersistence;composition 替换 .Jsonl / .Sqlite 时不替换 Coordinator。
能力清单与文档覆盖
仓库当前有约六十个程序集,本站不为每一个都写专页。下表按能力族给出全图,并如实标注哪些还没有专页。
| 族 | 程序集 | 专页 |
|---|---|---|
| 框架核心 | Cordis | 插件与 Context、服务与注入、事件系统 |
| 框架周边 | Cordis.Loader、Cordis.Scripting、Cordis.Hmr | 组合、脚本、HMR |
| 会话内核 | Tether.Core.Contracts、Tether.Core.Session | 会话与事件溯源 |
| 会话持久化 | Tether.Session.Persistence、.Coordinator、.Jsonl、.Sqlite | 会话持久化 |
| 会话投影 | Tether.Session.Projection | 投影注册表与缓存 |
| 会话领域投影 | Tether.Session.Title、.Stats、.Telemetry | 会话可观测性 |
| Agent | Tether.Core.Agent、.AgentLoop | Agent 生命周期 |
| 提示词 | Tether.Core.SystemPrompt | 系统提示词组装 |
| 工具管线 | Tether.Core.Tools | 工具管线 |
| 审批与交互 | Tether.Interaction | 审批与用户交互 |
| 横切校验 | Tether.Invariants | 运行期不变量 |
| 形式化建模 | formal/lean(TetherFormal,Lean 4) | 形式化建模 |
| 装配与入口 | Tether.Boot、Tether.Presets、Tether.Bundles、apps/Tether.Cli、apps/Tether.Desktop | 预设与运行时 Bundle、Composition Root、原生桌面壳 |
| 身份与配置 | Tether.Identity、Tether.Settings、Tether.Credentials | 设置、凭据与身份 |
| 自我演化回路 | Tether.Evolution*、apps/Tether.ShadowHost | 自我演化系统 |
| 模型可观测性 | Tether.Llm.Observability | LLM 流式与模型访问 |
| Web 与桌面平台 | Tether.Web.Client、Tether.Desktop.* | Web 客户端平台、原生桌面壳 |
这张表是快照,不是契约
包清单以 Tether.slnx 与源码树为准。项目正处在快速展开期,
新增能力包的速度快于文档补齐的速度——看到表里没有的目录属正常,
不代表它是实验性的。
其余程序集
| 程序集 | 引用 | 角色 |
|---|---|---|
Cordis.Scripting | Cordis | 源码编译为可卸载插件 |
Cordis.Loader | Cordis | YAML 组合与挂载 |
Cordis.Hmr | Cordis、Cordis.Scripting | 文件监听与热替换 |
Tether.Invariants | Cordis | 横切:运行期不变量注册表 |
Tether.Core.Session | Cordis、Contracts、Invariants | Provider |
Tether.Core.SystemPrompt | 同上 | Provider |
Tether.Core.Tools | 同上 | Provider |
Tether.Core.Agent | 同上 | Provider |
Tether.Interaction | 同上 | Provider(审批与提问) |
Tether.Core.AgentLoop | 加 Tether.Core.Agent、Tether.Llm | Consumer |
apps/Tether.Headless | 全部实现包 | Composition Root |
三点值得单独指出:
- 产品层实现包彼此不可见,仅通过服务接口相遇。唯一的例外是
Tether.Core.AgentLoop依赖Tether.Core.Agent。 - 只有拥有两个可独立观察、可能分叉的运行期事实的实现包才引用
Tether.Invariants并注册 package-owned 检查;没有这种关系的包不创建空 companion。详见运行期不变量。 Tether.Shell.Local与Tether.Fs.Search都额外引用Tether.Subprocess——前者用它起 bash / pwsh,后者用它跑打包的 ripgrep。进程执行只有一份实现。
只有进程入口引用全部实现包
apps/Tether.Headless 是唯一把所有实现包聚在一起的地方——这正是第三条规则的形状:
装配集中在一处,其余包互不感知。见 Composition Root 与运行。
目录约定
src/Cordis/ 框架核心,与 agent 无关,禁止引用任何 Tether.* 包
src/Cordis.*/ 框架周边(Loader / Hmr / Scripting)
src/Tether.*/ 产品层能力包
apps/ 进程入口(CLI / Web 宿主)
tests/ 与 src 镜像:src/Foo/Foo.csproj ↔ tests/Foo.Tests/Foo.Tests.csproj
formal/lean/ Lean 4 形式化对照模型(独立 Lake 工程,不进解决方案)
当前已落地的进程入口
apps/ 下目前有:
- Tether.Headless:无界面宿主,从 YAML 挂载完整插件树并把结果以 JSON 快照输出,兼具端到端验收载体;
- Tether.Cli:交互式终端命令行入口,支持会话交互、工具执行与插件管理;
- Tether.WebHost:ASP.NET Core / Web 宿主,提供实时推流、浏览器控制台与 REST 接口;
- Tether.Desktop:基于 PhotinoX 的轻量单进程原生桌面壳,内嵌 vendored 客户端并与 CLI 共享存储与 Profile;
- Tether.ShadowHost:演化评测专用的影子验证宿主,在隔离环境中验证冻结场景与回放回归。
既定技术选型
这些是已经拍定的决策,新包按此选型,不需要重新讨论。
| 子系统 | 技术 |
|---|---|
Context 点访问体验 | 第一版用 ctx.Get<T>() 朴素写法;partial interface IContext + Roslyn 源生成器聚合门面后补 |
| 配置校验 | DataAnnotations + Microsoft.Extensions.Options;复杂场景 FluentValidation |
| YAML 组合加载 | YamlDotNet + stable-id 有序补丁;Profile 支持 bundles: 有序栈驱动装配(#308);不做 !!js 表达式,用环境变量替换 |
| 热重载 | 可收集 AssemblyLoadContext(isCollectible: true) |
| 动态插件 | Microsoft.CodeAnalysis.CSharp 内存编译 + 自管可收集 ALC |
| LLM 流式 | HttpClient + SSE 解析成 IAsyncEnumerable<T>;抽象对齐 Microsoft.Extensions.AI.IChatClient;标准 OpenAI 走官方 Microsoft.Extensions.AI.OpenAI adapter(#470,钉死 MEAI.OpenAI 10.9.0 / OpenAI 2.12.0 / System.ClientModel 1.14.0,SDK 自带重试关闭),其余 wire 手解析 |
| RPC / SDK | StreamJsonRpc;Web 实时推流用 ASP.NET Core + SignalR |
| LSP stdio | 严格手写 Content-Length JSON-RPC framing;保持有界缓冲与零额外 transport 依赖 |
| 会话持久化 | provider-neutral coordinator + append-only ISessionEventStore;本地唯一 first-party provider 为 JSONL/Zstd(SQLite session 已按 #150 退役,SQLite 仅作为领域 KV 与观察索引),序列化用 System.Text.Json 源生成 |
| PTY | Windows ConPTY(P/Invoke),Unix openpty native broker |
| 沙箱 | 平台原生(macOS sandbox-exec seatbelt 已交付,Job Objects / landlock 后补;E2B 容器已按 #271 退役) |
工程规范要点
- C# 最新语言版本,
net10.0;Nullable与ImplicitUsings保持开启。 - 包版本统一由根目录
Directory.Packages.props(CPM)管理,csproj 的PackageReference不写Version。 - 异步 API 一律
ValueTask优先;禁止 sync-over-async(.Result/.Wait())。 - 公开 API 必须有 XML doc 注释,说明语义而不复述签名。
- 每个核心语义必须有单元测试,提交前
dotnet test全绿。 - 新能力包按 Service Definition / Provider / Consumer 三角色评估是否拆程序集。
已知风险
热重载卸载成功率。 ALC 卸载要求没有任何活引用指向旧程序集——事件处理器、静态缓存、timer 回调都算。注册即回收纪律是唯一保障。
沙箱跨平台。 全项目唯一无法纯托管解决的部分,首版容器优先压成本。
点访问 ergonomics。 ctx.tools 式体验需要源生成器聚合 partial interface,是计划内投入,但不要提前做。
首个 tag 之前:基础优先于兼容包袱
没有外部消费者,因此类型、命名空间、包划分可以自由重命名,但必须一次性更新所有引用。 各持久化 provider 的物理 schema / 日志格式不做向后兼容承诺,版本号单调递增即可。
范围与进度的事实来源
README 与本站都不维护实现进度或完成清单。以 GitHub 为唯一事实来源:
- dsh 能力对齐矩阵:固定基线、能力 owner、差异与验收义务
- Milestones:版本规划
- Issues:具体工作与依赖