dshkit

DeepSeek Harness 的 agent 预设:四种模式其实是四个目录

预设就是一个装着 agent.cordis.yml 的目录。自带的预设名单、为什么创作只能靠复制、「必须什么都没产出才能切」的规则、预设里的行怎么解析,以及坏掉的预设为什么被列出而不是跳过。

更新于 2026-08-145 min
一句话结论

agent 预设就是一个装着 agent.cordis.yml 的目录,按 agent 作用域挂载,所以一个进程能同时跑好几个组装方式不同的 agent。CLI 自带四个——standard、code、minimal、cordis——它们就是那四种 agent 模式。创作只能靠复制:把某个已有预设的整个目录复制到用户根目录下,然后改副本。

大家写来写去的「四种 agent 模式」,其实是四个目录。看懂这一点,整个定制化的故事就不神秘了——包括 怎么造第五种。

预设是什么

agent 预设就是一个装着一份 agent.cordis.yml 的目录。把它挂载到某个 agent 的作用域上下文下, 这个会话就有了自己的工具和提示词分区,而其它每个活着的会话仍然各用各的——所以一个进程可以同时跑 好几个组装方式不同的 agent。

两个包:

角色
agent-presets预设词汇、文件系统发现、受保护的按-agent 挂载ctx.agentPresets
persona把 agent 人设做成可组合的一行,于是预设能改的不只是工具,还有身份

自带名单

它们在 apps/cli/config/agent-presets/ 下,一个预设一个目录——而这份目录清单本身就是名单。 仓库刻意没有在文字里再列一遍,理由是:「在这里也写一遍,就等于多了一份要同步的清单,而且它会是最先 过时的那份。」

目录preset.yml 里的名字排序
standard标准模式1
codePTC 模式2
minimal极简模式3
cordis创造模式4

预设里能放什么、不能放什么

组装的切分原则是:注册表和跨会话设施是进程级单例,留在宿主组装里,而预设携带的是单个 agent 向它们贡献的东西。

一个预设如果引用了会发布进程全局服务的行,会在挂载时被拒绝,而不是被放行、然后和下一个会话撞车。 如果你确实需要按会话的服务,就用 isolate: 把它放进 entry-local 领域:

- id: svc
  name: ./plugins/global-service.js
  isolate:
    fixtureIsolatedSvc: true

同一个 provider 藏在 entry-local 领域后面就永远到不了根领域,于是它是按会话的而不是进程全局的—— 因此被接受。

下面是真实的 minimal 预设,它最能说明一份组装文件长什么样:

- id: persona
  name: '@deepseek-ai/dsh-persona'
  config:
    text: You are a helpful software engineer assistant.
    complete: true
    includeRuntimeContext: false
 
- id: persistent-shell
  name: cordis:group
  group: true
  isolate:
    terminals: true
  config:
    - id: pty
      name: '@deepseek-ai/dsh-terminal'
    - id: terminal-bash
      name: '@deepseek-ai/dsh-terminal-bash'
      config:
        timeoutMs: 300000
    - id: persistent-bash
      name: '@deepseek-ai/dsh-tool-bash-persistent'
      config:
        timeoutMs: 300000

注意 persona 上的 complete: true——人设本身就是完整的系统提示词,所以全局身份、工具指引和后续的 装配监听器都加不进任何提示词文本。而 includeRuntimeContext: false 关掉了运行时上下文快照。 「极简」就是这么做出来的:不是靠关功能,而是靠少组装几行

创作只能靠复制

只有一个写操作:copy(from, id, name?)。你复制的是某个已有预设的整个目录——组装文件、元数据、 skill 目录、资源——落到第一个 user 信任根下。

这个理由值得偷师:没有任何组装文本穿过服务边界,所以副本和它的源一样必定可加载,而一次复制不会 授予名单里本来没有的任何东西。创建之后的一切,都发生在预设自己的文件里。

copy() 会当场拒绝三件事:

  • 不符合 [a-z0-9][a-z0-9-]* 的 id。 id 会变成目录名,所以「不越界」是 id 自身的性质,而不是 事后再做一次路径检查——../escapea/b、绝对路径全部作为 id 被拒。
  • 已被占用的 id。 任何一个根提供了它就拒绝;磁盘上已有同名目录也拒绝。
  • 不存在的源。 复制失败会把做了一半的目录回滚掉。

复制出来的树会被重新收紧为仅所有者可访问(文件 0o600、目录 0o700),符号链接会被解引用, 所以副本是自包含的。复制出的 preset.yml 保留源的 description 供你就地编辑,但丢掉它的 name 和 排序 order——一个和源长得一模一样的副本,会让名单彻底分不清它俩。

remove() 拒绝删除随部署自带的预设。

切换,以及它为什么被锁

会话的创建头记录的是它启动时用的预设;resolveSessionPreset(session) 给出的是它当前在跑的 那个。空白会话切换过之后两者就不一致,所以每条重建路径——选择器摘要、resume、fork——都要走解析, 而不是直接读头。

切换是一条在交换提交之后追加的 agent-preset/selected 会话事件。这符合「模型可见 ⟺ 必须被记录」这条 规则:预设决定了模型看到的工具 schema 和提示词分区,所以它必须能从日志里重建出来。

recompose() 会卸载已装的子树再挂新的,因为两份组装无法共存——它们会把同名工具注册进同一层。 挂载失败会恢复原来的组装,而不是让 agent 变得一无所有。

预设里的行怎么解析

这是你自己写预设时最容易被咬的一处。

包名从宿主组装解析,不是从预设目录。 Loader 通常按自己那棵树的 baseUrl 解析条目,而对预设来说 那就是组装文件所在的位置。本地创作的预设住在用户 home 下,Node 向上找 node_modules 永远走不到 harness——于是每一行 @deepseek-ai/dsh-* 都会 import 失败。挂载过程会在插入子树之前记录宿主基址, 并把裸标识符发到那里去解析。

相对路径仍然从预设自己的目录解析,所以预设自带的插件文件和 skill 目录能跟着它走。

绝对路径保留自己的位置,在 ESM import 之前被转成 file: URL,这样 POSIX 路径和 Windows 盘符 或 UNC 路径都能用。

子 agent 是「加入」,不是「重新挂载」

子 agent 通过 composeFrom() 加入父级已有的常驻组装,绝不走 mount()。两个理由都很实在:

  • 父级启动之后被编辑过的组装文件,会给子 agent 一个和父级历史所依据的那一代不同的组装。
  • 而如果预设在这期间被删了,子 agent 会直接失败,父级却还在跑。

这个绑定还是同步的,正因如此进程内的子 agent 驱动才用得上它——它们是在一个同步的创建窗口里组装子级的。

还有一个值得知道的后果:所有面向模型的行都活在 agent 平面上,所以工具注册表的全局层是空的。 一个什么都没加入的子 agent,到达模型时一个工具都没有,父级的提示词分区也一个都没有。

发现、健康度与陈旧

发现是不做记忆化的:list()resolve() 每次调用都重读各个根,所以进程运行期间新写的预设 立刻可见。

组装文件缺失或加载不了的目录会带着一条 broken 原因被列出来,而不是被跳过——因为跳过之后,那个 目录仍然占着磁盘上的 id,而所有界面上却看不到有什么可删。而目录名本身就不是合法预设 id 的,会被直接 跳过,因为任何副本都不可能占用那个名字。

每一代挂载都会记录组装文件的戳记(mtime 和大小)。发现戳记过期的会话会开启下一代, 而已经加入的每个会话仍然留在它正在跑的那一代——所以一个运行中会话所加入的组装,能挺过它的文件 在脚下被改动或删除。

常见问题

四种 agent 模式到底在哪?

在 apps/cli/config/agent-presets 下,就是 standard、code、minimal、cordis 四个目录。每个 preset.yml 里带着显示名——code 是「PTC 模式」、cordis 是「创造模式」——所以大家在讨论的那些模式,就是这些预设目录。

为什么不能通过 API 从零写一个预设?

创作只能靠复制是刻意设计:没有任何组装文本穿过服务边界,所以副本和源一样必定可加载,而且一次复制不会授予名单里本来没有的任何东西。

为什么不能在对话中途换预设?

中途换工具会留下一堆新组装做不到的已记录工具调用。这是产品规则,在网关层强制执行,会回 agent-preset-locked。

坏掉的预设为什么还出现在列表里?

如果直接跳过,那个目录仍然占着磁盘上的 id,而所有界面上却看不到有什么可删。所以组装文件缺失或加载不了的预设会带着一条 broken 原因被列出来。

接着看