第 2 周 · Tether 源码课

把大型仓库读成一张地图

预计 75–90 分钟 先修:第 1 周:第一次运行、知道 class 与 interface 的区别

学完你能做到

  • 用 solution、project、reference、test 四个词描述 .NET 仓库
  • 从目录名判断 Cordis 框架层与 Tether 产品层
  • 沿一个能力找到 seam、Provider、Consumer 和镜像测试
  • 只运行与当前问题有关的一组测试

课程进度

  1. 第 1 周
  2. 第 2 周
  3. 第 3 周
  4. 第 4 周
  5. 第 5 周
  6. 第 6 周
  7. 第 7 周
  8. 第 8 周
  9. 第 9 周
  10. 第 10 周
  11. 第 11 周
  12. 第 12 周
  13. 第 13 周
  14. 第 14 周
  15. 第 15 周
  16. 第 16 周

一句话先懂

大型仓库不是一本必须从第一页读到最后一页的小说,而是一座有路牌的城市。

Tether.slnx 是总地图,目录是街区,.csproj 的 ProjectReference 是道路,测试项目是写明道路规则的可执行说明书。

这一周不追业务流程。你要学的是一种更耐用的能力:面对几十个项目时,先找边界和关系,再决定读哪几行代码。

先看大图

运行入口 apps/ CLI · Headless:读取参数,启动 Boot 很薄;不在这里堆插件实现 产品层 src/Tether.*/ Agent · Session · LLM · Tools · Fs · Shell 能力通常按 seam / Provider / Consumer 分开 知道什么是 agent,不把细节塞回 Cordis 框架层 src/Cordis*/ 插件、作用域、服务、事件、加载与热重载 tests/ 镜像源码项目 Cordis.Tests Tether.Fs.Local.Tests Tether.Core.AgentLoop.Tests 测试不是附录 它把语义变成断言 只读当前问题 对应的那一组
先分层,再找一条能力链,最后用镜像测试确认语义。依赖方向比文件数量更重要。

一个类比:城市地图与道路

  • Solution 像城市总图:列出有哪些 project,但不等于它们被合成一个巨大程序。
  • Project 像一个街区:有自己的边界、编译产物与公开入口。
  • ProjectReference 像单向道路:A 引用 B,表示 A 编译时可以使用 B;不代表 B 也能看见 A。
  • Test project 像道路法规与验收站:它通过真实输入和断言说明“这条路应当怎样工作”。

类比边界:.NET 程序集不是物理街区。运行时加载、NuGet package、internal/public 可见性和依赖注入都比道路类比精细;今天只用它理解静态项目边界与引用方向。

四个先学会的词

词在仓库里怎么看你要问的问题
Solution根目录 Tether.slnx这座仓库包含哪些可构建项目?
Project一个 .csproj 及其源码它对外承诺什么职责?
Reference.csproj 中的 ProjectReference依赖从谁指向谁?有没有越层?
Testtests/X.Tests/哪条调用方可观察语义被固定下来?

打开 Tether.slnx,你会看到项目按 apps、src、tests 分组。它是一份清单,不是学习顺序。学习顺序由问题决定。

三层,而不是六十个孤岛

Cordis:不知道 Agent 是什么

src/Cordis/ 是通用插件框架。它可以认识 Context、service 和 event,却不能引用任何 Tether.* 包。这样框架层不会被产品概念反向污染。

Tether:把 Agent 能力放在 Cordis 之上

src/Tether.*/ 包含 LLM、session、tool、filesystem、shell 等产品能力。它们借 Cordis 挂载和连接,但 Cordis 不需要知道这些能力的含义。

Apps:只负责把程序启动起来

apps/Tether.Cli 与 apps/Tether.Headless 是进程入口。它们处理参数、日志和宿主生命周期;插件树由 Boot 与 YAML composition 装配。

完整分层规则可查架构总览。今天只记一句:越稳定、越通用的规则越靠下;依赖只向下。

源码放大镜:一个能力的三种角色

用文件系统能力做例子:

src/Tether.Fs/          seam:接口与数据类型
src/Tether.Fs.Local/    Provider:本地磁盘实现
src/Tether.Fs.Tools/    Consumer:把文件能力暴露成模型工具
tests/Tether.Fs.Local.Tests/  镜像测试

先打开 src/Tether.Fs/Tether.Fs.csproj。它没有任何 ProjectReference:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
  </PropertyGroup>
</Project>

再看 src/Tether.Fs.Local/Tether.Fs.Local.csproj 的关键引用:

<ProjectReference Include="..\Cordis\Cordis.csproj" />
<ProjectReference Include="..\Tether.Fs\Tether.Fs.csproj" />
<ProjectReference Include="..\Tether.Invariants\Tether.Invariants.csproj" />
<ProjectReference Include="..\Tether.Sandbox\Tether.Sandbox.csproj" />

关系是单向的:Provider 知道 seam;seam 不知道本地 Provider。以后替换成远程或沙箱实现时,Consumer 仍只认同一个接口边界。

Tether.Fs seam · 接口边界 Tether.Fs.Local Provider · 本地实现 Tether.Fs.Tools Consumer · 模型工具 Fs.Local.Tests 验证调用方可见行为
箭头表示“引用”。Provider 与 Consumer 可以替换,seam 保持稳定。

动手实验:只问仓库一个小问题

实验目标:不运行全仓库测试,只验证“无依赖插件会立即 apply”这一小块 Cordis 语义。

1. 预测

先打开 tests/Cordis.Tests/PluginTests.cs,只读前两个测试名:

Plugin_without_inject_applies_immediately
Plugin_waits_for_injected_services_then_applies

预测:两者唯一关键差别是什么?第二个插件要等到哪件事发生?

2. 运行

dotnet test tests/Cordis.Tests/Cordis.Tests.csproj \
  --filter FullyQualifiedName~PluginTests

PowerShell 同样可以使用反引号换行,或把命令写在一行。--filter 让测试 runner 只选择全名包含 PluginTests 的测试。

3. 观察

  • 编译只会拉入 Cordis.Tests 所需的依赖,不会启动 CLI。
  • 测试 runner 会列出通过/失败数量;正常基线应全部通过。
  • 测试名已经表达行为,比“test1”“works”更像一条可执行规格。

4. 解释

你刚才沿这条路线阅读仓库:

问题:插件何时 apply?
  → tests/Cordis.Tests/PluginTests.cs
  → src/Cordis/Plugin.cs + Registry.cs
  → 运行过滤后的测试验证理解

这比从 src/Cordis/Context.cs 第一行开始漫无目的地读,更接近真实软件工程工作。

地图练习

在 Tether.slnx 中任选一个带 .Local 的项目,完成下表:

要找的东西你的答案
seam 项目例如 Tether.Fs
Provider 项目例如 Tether.Fs.Local
它引用的 seam查看 .csproj
镜像测试项目查看 tests/
一个说明行为的测试名不要只抄 class 名
查看一个参考答案

Tether.Shell.Local 是本地 Shell Provider,引用 Tether.Shell seam;镜像项目是 tests/Tether.Shell.Local.Tests。具体测试名应以你当前检出的源码为准。

检查理解

1. Tether.Fs.Local 引用 Tether.Fs,能否因此推出 Tether.Fs 也引用 Local?

查看答案

不能。ProjectReference 是单向边。恰恰因为 seam 不引用 Provider,调用方才能在不改接口边界的情况下替换实现。

2. 为什么测试项目要镜像源码项目,而不是把所有测试放进一个巨大项目?

查看答案

镜像结构让依赖和职责清晰:看到一个源码项目就能找到对应测试,也避免一个“万能测试项目”不受控制地引用所有实现、掩盖错误分层。

3. apps/Tether.Cli 能引用很多实现包,是否违反依赖只向下?

查看答案

不违反。进程入口处于最上层,负责选择和启动实现。规则禁止的是底层 Cordis 或 seam 反向依赖上层产品与具体 Provider。

本周带走

  • 先用 solution 找项目,用 .csproj 找依赖,用测试找语义。
  • Cordis 是通用框架层,Tether 是产品层,apps 是 composition root 的进程外壳。
  • seam、Provider、Consumer 的引用方向让实现可替换;目录命名本身就是路牌。

下一周开始进入最底层的日常入口:Plugin 与 Context。如果想先查看完整依赖图,可读架构总览与能力服务。

在 GitHub 上编辑此页