预设与运行时 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.yamlbase + replay LLM/default-model replacement;其它能力继续复用 base rows
cli.yamlbase + 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 实施分级回退策略:

  1. 精确认领:优先按历史记录的 (presetId, contentHash) 获取 exact generation;
  2. 版本演进回退(GenerationMissing):若该特定 hash 的 generation 已随版本升级注销,回退至同名 preset 的当前活跃 revision(expectedContentHash: null);
  3. 预设移除回退(NotFound):若原 preset 已被用户配置彻底删除,回退至系统默认 preset;
  4. 兜底无预设启动:若连默认 preset 认领也失败,允许以无预设的裸 Agent 恢复会话,确保任何历史投影与会话轨迹永远能正常加载并可读。
  5. 持久化零篡改:回退仅影响内存中的运行时租约,绝不篡改底层已持久化的 SessionHeader.Preset 或历史 agent-preset/selected 事件。

bundle 只声明 rows,patch 语义归 Cordis.Loader。所有路径用 $\{TETHER_HOME\} 等环境变量插值,与 dsh 的 profile/bundle 分层思路一致:bundle 插入的一切都可被它之上的层继续 patch。

下一步

在 GitHub 上编辑此页