预设与运行时 Bundle
同一个 harness,换一个 preset 就是另一个 Agent——自己的提示词、自己的工具集、互不泄漏的监听器。
运行时整体形状则由 bundle 决定。对应实现:src/Tether.Presets/ 与 src/Tether.Bundles/。
Preset 声明
一个 preset 是一份声明式定义,经 agent-preset 组合行声明进 roster:
- id: preset-standard
name: agent-preset
config:
id: standard
order: 1
plugins:
- { name: persona, config: { prefix: "...", includeRuntimeContext: true } }
- { name: scoped-tool-guidance, config: { toolPresentation: native, toolDenyList: [...] } }
- { name: terminal-tools }
Tether 自有字段(prompt、toolAllowList/toolDenyList/toolPresentation 等)不留在 preset 顶层,而是落进 plugins 列表里 persona / scoped-tool-guidance 子行的 config。AgentPresetDefinitionValidation 做结构校验:行键只允许 id/name/disabled/config,group/isolate 作为无效拒绝(flat compositions),disabled 必须是布尔字面量,id 必须是 [A-Za-z0-9_-]+。结构问题以 PRESET_INVALID 让行本身失败;挂载失败则以 Broken 原因留在 roster 里——声明不阻止其余 preset 与组合启动。
AgentPresetRegistry(agent-preset-registry 行提供,注入 IAgentPresetRegistry/IAgentPresetPackages)是 roster 与选择的唯一权威:RegisterAsync/UnregisterAsync/List/ResolveDefaultId/AcquireAsync/SelectAsync/ComposeFromAsync/CompositionInventoryAsync。四个随包 preset(standard/ptc/minimal/cordis)以 web bundle 的 patch 行发行;用户旧 *.preset.yaml 目录可经 legacyPresets 配置一次性导入为 patch 行。
name/description 是可选的展示元数据,不参与 generation 内容 hash。随包 preset 与上游一样不声明它们:Web 客户端只把“没有 name 且 id 属于 standard/ptc/minimal/cordis”的行视为内置,按 id 显示本地化名称、说明与模式帮助;声明了 name 的 preset 归入“自定义”,原样显示自己的文案。agentPresets/list 与插件清单只发布声明过的元数据,不以 id 兜底。
Generation 化挂载
registry 把每个 preset 变成以规范化内容 hash 标识的 standing generation(PresetGenerationPool 拥有 revision):
- 注册即尝试从进程根挂载一个带注册边界的 generation Context;persona、tool 与 listener 必须登记进该 generation 的
AgentPipelineScope,试图向共享 root 注册会让该行失败并写入Broken。 - Agent publication 为 exact generation 取得 lease,并把自己的 pipeline scope 接到该 generation scope;不同 generation 的能力不会通过共享 Cordis registry 互串。
- 同 id 重新注册生成新 generation;被 live Agent lease 或 durable Session pin 引用的旧 generation 仍可按 stamp 取得(
AcquireAsync(id, expectedContentHash)),供 resume/cold rebind/subagent 继承父 exact generation(ComposeFromAsync)。 - 一个失效 generation 的 standing、live 与 durable 引用全部释放后,才按 EffectScope 生命周期卸载其 Context、listener、tool、persona 与 ALC 引用。
因此隔离依据不是“恰好创建了两个 Context”,而是 exact content-hash identity、显式 pipeline parent 与引用计数共同形成的 authority。
默认模型
AgentDefaultModelService 把”用户没显式指定模型时用什么”收敛成一个可写设置:
Base是编译进 bundle 的兜底(TETHER_DEFAULT_ROUTE/TETHER_DEFAULT_MODEL环境变量注入)。- 用户层设置命名空间(默认
agent)里保存的选择覆盖Base;SaveSelectionAsync写回设置层。
Agent 路由解析顺序:Agent 显式钉住的 AgentOptions → 默认模型服务 → bundle 兜底。
运行时 Bundle
src/Tether.Bundles/bundles/ 里三份 YAML 是发行形状的声明:
| Bundle | 形状 |
|---|---|
base.yaml | 每个运行时都叠的第一层:会话/Agent 内核、JSONL session-store 与通用 Coordinator、settings / credentials / identity、attachment/spill、本地执行世界、multi-provider LLM、默认模型、title 与 telemetry。Preset roster(agent-preset-registry + agent-preset 行 + preset-selection)只在 web profile 发行 |
headless.yaml | base + replay LLM/default-model replacement;其它能力继续复用 base rows |
cli.yaml | base + replay LLM/default-model replacement + projection cache 与交互/CLI consumers |
Preset identity 已是 durable fact:SessionHeader(格式 v7 起)记录 session 的
initial preset stamp(<preset-id>@<content-hash>,hash 由 preset 文档内容铸造),
agent-preset/selected 事件在 publication fence 内、header mint 与 Agent 可见之前提交。
空白 session(日志中尚无 turn/start)可切换 preset:先 mount/validate 新 generation
再提交事件,失败保持原 identity;cold read、resume 与 fork 只从 header + ordered selected
事件解析 authoritative identity,不重读当前默认,header/event 冲突按 session corruption 处理。
preset 重新声明(新 revision)只影响随后创建/选择的 generation,已有 session 的 recorded identity 不重绑;
provider/preset service HMR 也不能使仍被 lease 或 durable pin 持有的 generation 消失。
会话恢复的分级容错(Generation-Missing 回退)
历史是 durable 数据,preset 是运行时配置;preset 的自然演化(如随版本新增默认能力行使 content hash 改变)绝不阻断历史读取。PresetSelection.PrepareLeaseUnderGatesAsync 实施分级回退策略:
- 精确认领:优先按历史记录的
(presetId, contentHash)获取 exact generation; - 版本演进回退(GenerationMissing):若该特定 hash 的 generation 已随版本升级注销,回退至同名 preset 的当前活跃 revision(
expectedContentHash: null); - 预设移除回退(NotFound):若原 preset 已被用户配置彻底删除,回退至系统默认 preset;
- 兜底无预设启动:若连默认 preset 认领也失败,允许以无预设的裸 Agent 恢复会话,确保任何历史投影与会话轨迹永远能正常加载并可读。
- 持久化零篡改:回退仅影响内存中的运行时租约,绝不篡改底层已持久化的
SessionHeader.Preset或历史agent-preset/selected事件。
bundle 只声明 rows,patch 语义归 Cordis.Loader。所有路径用 $\{TETHER_HOME\} 等环境变量插值,与 dsh 的 profile/bundle 分层思路一致:bundle 插入的一切都可被它之上的层继续 patch。