DeepSeek Harness 完整学习与实战笔记

目处于开发者预览阶段,命令和 API 可能发生破坏性变化;发生冲突时,以链接的源码、包 README 和生成目录为准。

1. 一页速览

DeepSeek Harness(命令名 dsh)不是一个写死流程的聊天应用,而是一个由 Cordis 驱动的可组合智能体框架。LLM 适配器、Agent Loop、工具、会话日志、持久化、沙箱、Web UI 都是插件;Profile 选择一组 Bundle,Bundle 提供补丁层,用户仍可通过 cordis.patch.yml--patch 替换任意配置行。

最快体验:已发布版本使用 npx @deepseek-ai/dsh web;源码版本在仓库根目录依次运行 pnpm installpnpm run buildpnpm dsh web,然后访问 http://127.0.0.1:3080

2. 本机验证结论

项目 本机结果
Linux / Node.js Linux x64;Node v24.14.1,满足 `^22.19.0
pnpm 通过 Corepack 启用仓库固定的 11.7.0
Python / Git Python 3.12.8;Git 2.34.1
依赖安装 成功;共 238 个 workspace project、923 个依赖包
原生依赖 node-pty 使用 Node 安装目录内的 24.14.1 头文件完成本机编译
构建 pnpm run build 成功,Host、Client、Web 前端均生成产物
类型检查 pnpm run typecheck 成功
实战类型检查 greet-plugin/tsconfig.json 仅消费构建后的公共声明,检查成功
针对性测试 CLI 参数及 base/web-app/headless Bundle:4 个文件、25 项测试全部通过
自定义模型 /v1/models/v1/chat/completions 均 HTTP 200;默认 deepseek-v4-flash
浏览器 自定义 Provider 和 4 个 Demo 模块已启用;真实模型依次调用两个 Tool 成功
私有配置 仓库根 .env 已填写 Key 且被 .gitignore 忽略;验证过程未输出 Key

安装时如果 node-pty 因无法下载 Node 头文件失败,而本机使用 NVM,可将 npm_config_nodedir 指向当前 Node 安装目录后重试。不要用版本不匹配的系统头文件。

3. 安装、运行与凭据

3.1 前置条件

  • •Node.js 22.19+,或 24+;CI 覆盖 22.19、24、26。
  • •Corepack 管理的 pnpm 11.7.0。
  • •Git 2.26+。
  • •Python SDK 路线需要 Python 3.10+;支持 Linux x64/arm64、macOS 14+ arm64。
  • •真实模型调用需要 DeepSeek 兼容端点和 API Key;单纯启动 Web UI、浏览设置与运行 keyless 测试不需要 Key。

3.2 源码路线

  1. •在仓库根目录运行 corepack enable,再确认 pnpm --version 为 11.7.0。
  2. •运行 pnpm install。安装脚本会设置 worktree-local Lefthook 和翻译合并驱动。
  3. •运行 pnpm run build。源码 CLI 虽通过 TSX 启动,但 Typert Host 产物和 Web/Client bundle 仍须预先构建。
  4. •运行 pnpm run typecheck 验证环境。
  5. •运行 pnpm dsh web,访问终端打印的 URL,默认是 http://127.0.0.1:3080

3.3 API 地址与 Key 填在哪里

本实验室已在仓库根创建被 Git 忽略的 .env

复制代码
DEEPSEEK_API_KEY=

使用公共 DeepSeek API 时只填 Key。接入自定义地址有两种不同机制:官方 dsh-llm-deepseek route 的端点覆盖使用启动 shell 中的 DEEPSEEK_BASE_URL;它是 bootstrap-only 变量,产品启动器会拒绝它出现在 .env。本教材第 7 章使用更通用的 dsh-llm-pi-ai 手工 route,地址直接写在不含机密的 overlay baseURL 中,因此不需要 DEEPSEEK_BASE_URL。不要把真实 Key 发到聊天、日志或提交中。Web 用户也可在"设置 → 模型"添加自定义 Provider:明文密钥写入 $DSH_HOME/.credentials.yaml(默认 ~/.dsh/.credentials.yaml,权限 0600),settings.yaml 只保存引用,浏览器只收到脱敏描述符。

API Key 等普通凭据的优先级从高到低为:启动进程继承的环境变量 → $DSH_HOME/.credentials.yaml → 启动目录 .env$DSH_HOME/.env。调用目录 .env 只在启动时读取一次,不会跟随后来在 Web 中选择的 workspace。官方 Adapter 的 DEEPSEEK_BASE_URL、代理地址等 bootstrap-only 变量只能来自启动环境,不参与文件层;pi-ai route 的非机密 baseURL 是插件配置,不受该限制。

$DSH_HOME/.credentials.yaml 是扁平 YAML mapping,例如 DEEPSEEK_API_KEY: sk-...,不是嵌套的 deepseek.apiKey。启动环境变量始终压过 UI 管理的文件值;若想让 UI 写入值生效,不要在启动 shell 中留下同名旧变量。

3.4 CLI 入口

命令形态 用途
pnpm dsh web 启动 Web Profile;默认只绑定 127.0.0.1:3080
pnpm dsh web --port 8080 Web 应用参数必须位于 launcher 参数之后
pnpm dsh --profile headless "任务" 创建一个新会话,完成一次任务,打印最终回答并退出
pnpm dsh --profile web --dump-default-config 仅打印 Bundle 层,不启动插件
pnpm dsh web --patch ./extra.yml --dump-config 打印包含用户层、Home 层和 overlay 的最终树
pnpm dsh plugin --profile web add <包> 在 Profile 目录中通过 pnpm 安装外部插件/Bundle
pnpm dsh plugin --profile web remove <包> 从 Profile 移除外部插件/Bundle(详见第 11 章)
pnpm run demo:acp 启动 ACP stdio 自动化服务
pnpm run demo:cordis 运行可检查、修改自身插件树的演示

Launcher 参数在前;第一个不属于 Launcher 的参数开始都交给应用。Web 支持 --host--port、可重复的 --trusted-host。当前故意拒绝 --host 0.0.0.0。Headless 只接收一个非空任务文本,不启动 HTTP、Host 或浏览器。

3.5 Python SDK

创建虚拟环境并安装 deepseek-harness-sdk。已发布 wheel 自带匹配的运行时,目标机器无需 Node.js。仓库的完整可运行入口是 examples/jsonrpc-agent/minimal.py,组合文件是 minimal.cordis.yml

运行时必须明确传入隔离 workspace、session root 和 session id。Minimal 组合使用 danger-full-access,持久 Bash 与编辑器可访问运行时用户能访问的任意路径,只应对一次性 checkout 或容器使用。复用同一 session id 会延续对话和持久 PTY 状态;独立任务使用不同 id。

4. 顶层目录与根文件地图

路径 作用
apps/ 产品入口:cli/ 是 Profile Launcher,web/ 是 Vite 浏览器前端
assets/ 根 README 使用的社区二维码等静态资源
docs/ 架构、子系统、教程、用户指南、生成目录、中英配对文档
examples/ 可运行组合:ACP、Headless、JSON-RPC、MCP、Web overlay
native/ Linux Landlock 自限制启动器及平台 npm 包
packages/ 49 个能力组、219 个左右的工作区包;产品主体
patches/ pnpm dependency patch,目前包含 node-pty@1.1.0.patch
python/ sdk/ Python API 与 sdk-runtime/ 内置运行时打包
scripts/ 构建门禁、生成器、文档校验、发布与仓库维护脚本
vendor/ 仓库内维护的 Cordis 等 vendored 依赖;改动前读 vendor/README.md
website/ VitePress 文档站;docs.ts 决定仓库文档到站点的投影
.agents/ Agent Notes、技能和 agent 工作约定;决策理由的权威位置
.github/ CI、Issue 管理和 GitHub 自动化
package.json Monorepo 脚本、Node/pnpm 版本、工作区入口
pnpm-workspace.yaml / pnpm-lock.yaml 工作区范围、依赖补丁与可复现锁文件
tsconfig.base*.json Host/Client 共享编译面与 paths;根 tsconfig.json 是 solution
tsconfig.host.json / tsconfig.client.json 互相隔离的 Host 与浏览器 aggregate program
tsdown.config.ts 两阶段包构建,Host 阶段运行 Typert,Client 阶段构建浏览器面
vitest*.config.ts 单元、e2e、快照、Web、性能、压力等测试分层
AGENTS.md 全仓开发约定;修改代码前必读
README*.md 产品介绍和最短启动路径
CONTRIBUTING*.md 贡献政策与流程
BENCHMARK.md 基准测试入口,指向 Python SDK + JSON-RPC minimal 组合
LICENSE / THIRD_PARTY_NOTICES.md MIT 许可证和第三方声明

lib/apps/web/dist/node_modules/.sessions/.storages/.artifacts/ 都是生成或本机状态,不应提交。

5. packages/:49 个能力组与全部子包

每个组的 README.md 是该组"包 → 职责 → ctx key"映射的权威来源。以下列表用于导航,具体 API 以各包 README、JSDoc、docs/config-catalog.mddocs/tool-catalog.md 为准。

  1. core/:产品 API 脊柱。scopesessionsystem-prompttoolsagentagent-default-modelagent-tool-presentationagent-loop
  2. api/:Remote BFF 与 Typert RPC。remotesgateway
  3. attachment/:耐久附件身份与本地内容存储。attachmentattachment-local
  4. boot/:共享启动胶水。app-bootcmdline
  5. bundle/:可安装 Profile 补丁层。baseheadlessweb-app
  6. client/:浏览器运行时和 UI 插件。webmodulesweb-reactconnectionruntimehmrlocaleschema-formui-slotsui-themeui-primitivesui-attachmentui-layoutui-sidebarui-workspaceui-conversationui-toolui-workflow-runui-goalui-trajectoryui-commandsui-input-triggerui-skillui-subagentui-jobsui-model-selectionui-permission-presetsui-planui-settingsui-settings-generalui-settings-modelsui-settings-plugin-inventoryui-settings-pluginsui-user-questionsui-agent-presetui-message-feedbackui-deliverablesui-directory-picker-browseui-directory-picker-native
  7. code-runtime/:代码执行服务与 Worker Thread Provider。code-runtimecode-runtime-worker-thread
  8. compaction/:上下文压缩。compactioncompaction-basiccommand-compactcompaction-tool-result-pruner
  9. context/:模型可见请求上下文。agent-instructionssession-referencetime-contexttmux-context
  10. credentials/:凭据引用与本地分层存储。credentialscredentials-local
  11. e2b/:E2B 沙箱 POC。e2bfs-e2bsubprocess-e2b
  12. examples/:示例所复用的应用脊柱和 bin。acp-demoagent-spine-demojsonrpc-demo
  13. extensions/:运行时自检与自修改。cordis-client-runnercordis-host-runnertool-cordisui-cordis
  14. feedback/:人类反馈。command-feedbackmessage-feedback
  15. fs/:文件系统 seam、Provider 与工具。fsfs-localfs-sandboxfs-observation-policytool-fstool-fs-searchtool-str-replace-editor
  16. goal/:同会话目标状态。goalgoal-round-drivercommand-goaltool-goal
  17. guard/:Loop 卫生与超时。repeat-tool-remindertimeout-policy
  18. hooks/:Claude Code/Codex Hook Bridge。hook-protocolhooks-claude-codehooks-codex
  19. host/:Web Host 半边。webserverapiproxydirectory-pickerdirectory-picker-autodirectory-picker-browsedirectory-picker-nativefrontend-staticplugin-inventory
  20. identity/:共享匿名身份。anonymous-user-id
  21. interaction/:命令、审批、权限与提问。commandspermission-presetstool-ask-useruser-approvaluser-questions
  22. jobs/:后台作业。jobsjobs-localtool-jobs
  23. llm/:LLM seam 与适配器。llmtoken-meterllm-retryllm-deepseekllm-pi-ai
  24. lsp/:语言服务器能力。lsplsp-stdiotool-lsp
  25. mcp/:通用 MCP 客户端。mcp-client
  26. plan/:可审阅退出的计划协作状态。plan-mode
  27. preset/:每会话 Agent 组合。agent-presetspersona
  28. runtime-diagnostics/:开发时运行不变量。invariants
  29. sandbox/:进程限制 seam。sandboxsandbox-localsandbox-policysandbox-windows-acl
  30. schedule/:会话内定时跟进。schedule
  31. sdk/:进程外 JSON-RPC SDK。clientprotocolserver
  32. session/:持久化、投影、标题、统计、遥测。session-persistencesession-checkpoint-policysession-persistence-jsonlsession-persistence-sqlitesession-projectionsession-projection-cachesession-statssession-titlesession-title-llmsession-title-first-prompt-llmsession-title-all-prompts-llmsession-telemetrysession-telemetry-otel
  33. session-query/:会话读取、谱系、全文检索和导出。session-querysession-query-sqlitesession-log-exporttool-session-query
  34. settings/:用户设置 seam。settingssettings-file
  35. shell/:Shell seam、本地/沙箱 Provider 和模型工具。shellbash-localbash-sandboxpwsh-localpwsh-sandboxshell-envtool-bashtool-pwshtool-bash-persistent
  36. skill/:技能注册、文件发现和模型工具。skillskill-badgeskill-filesystemtool-skill
  37. spill/:超大工具结果溢出。spillspill-localspill-policy
  38. storage/:非会话通用存储。storagestorage-domainstorage-jsonstorage-sqlite
  39. subagent/:子 Agent seam、Provider 和控制工具。subagentsubagent-acpsubagent-claude-codesubagent-codexsubagent-dsh-sdksubagent-fork-in-processsubagent-spawn-in-processsubagent-in-process-drivertool-subagenttool-subagent-controltool-subagent-report
  40. subprocess/:托管子进程树。subprocesssubprocess-local
  41. terminal/:持久 PTY。terminalterminal-bashtool-terminal
  42. test-support/:测试基础设施。acp-snapshotagent-loop-testkitclient-runtimeloader-smokellm-mock-serverllm-replay
  43. todo/:模型任务清单。tool-todo
  44. typert/:类型图生成、装载、协议与注册。generatorloaderprotocolregistry
  45. util/:零 Harness 依赖工具。atomic-writebrandhome-pathslaunch-environmentnative-commandoutput-retentiontimeout
  46. web/:搜索/抓取 seam 与 Provider。webweb-fetch-httpweb-search-deepseekweb-search-exaweb-search-perplexitytool-web
  47. workflow/:动态工作流 seam。workflowworkflow-worker-threadtool-workflowtool-ralph
  48. workspace/:工作区实体。workspace
  49. acp/:自动化专用 Agent Client Protocol 服务。acp

依赖方向的核心纪律:扩展依赖 Service Definition,不依赖具体 Provider;组合 Bundle 可以依赖完整脊柱。完整依赖图由 docs/module-graph.md 生成,不应手抄维护。

6. 从零理解运行时:插件、服务、事件和生命周期

这一章不是阅读清单,而是后续开发必须掌握的概念正文。先理解"谁拥有状态、谁负责清理、调用怎样穿过系统",再写代码会省掉大量调试时间。

6.1 Cordis Context:不是全局变量,而是当前插件可见的能力集合

每个插件的入口都会收到 ctx: Contextctx.toolsctx.llmctx.sessions 等属性是其他插件提供的服务;ctx.on()ctx.effect()ctx.plugin() 则用于贡献行为。Context 还携带当前作用域,所以同一个服务名在不同隔离 realm 中可以解析到不同实例。

最小函数插件只有一个 apply

复制代码
import type { Context } from '@deepseek-ai/cordis'export const name = 'hello-plugin'export function apply(ctx: Context): void {  console.log('plugin loaded')}

对象插件把 nameinjectapply 放在一个默认导出对象中;类插件通常继承 Service,用于向 ctx 提供命名 API。简单行为优先使用函数插件,只有确实拥有服务状态时才使用类。

6.2 Fiber:每个插件实例的生命周期所有者

Cordis 为每个插件实例创建一个 Fiber。状态机是:

复制代码
PENDING → LOADING → ACTIVE                 ↘ FAILEDACTIVE → UNLOADING → DISPOSED
  • PENDING:配置行存在,但 inject 的依赖尚未全部出现。
  • LOADING:依赖就绪,正在运行 apply 或构造类插件。
  • ACTIVE:插件已提供服务、监听器或工具。
  • FAILED:加载过程抛错;错误应直接修复,不要静默跳过。
  • UNLOADING:依赖消失、配置热替换、父插件卸载或主动 dispose,正在清理。
  • DISPOSED:该实例的贡献和子 Fiber 已全部撤销。

const fiber = ctx.plugin(plugin, config) 会创建子 Fiber;await fiber.dispose() 会卸载它、递归卸载子插件并等待异步清理完成。

6.3 Effect:所有注册都必须可逆

Harness 的核心纪律是"注册即 Effect"。ctx.on()、服务注册、ctx.tools.register() 等 API 自己返回或内部登记 disposer;自有资源用 ctx.effect()

复制代码
export function apply(ctx: Context): void {  ctx.effect(() => {    const timer = setInterval(() => console.log('tick'), 5000)    return () => clearInterval(timer)  })}

卸载时 disposer 以注册顺序的逆序开始调用,但多个异步 disposer 可能并发完成。如果关闭 B 必须发生在关闭 A 之后,就把两步放进同一个 disposer 并显式 await,不要依赖两个 Effect 的完成顺序。

为什么重要:HMR、Profile 修改、Provider 替换、Agent Preset 卸载都会触发生命周期变化。绕过 Effect 注册的 timer、socket、event listener 会变成幽灵资源。

6.4 inject:依赖关系,不是建议

复制代码
export const inject = ['tools', 'textTransform']export function apply(ctx: Context): void {  ctx.tools.register(/* ... */)  void ctx.textTransform}

Loader 不依赖 YAML 行顺序决定加载;它等待 toolstextTransform 都可见后才执行 apply。必需服务消失时,消费插件自动卸载;服务恢复时重新加载。可选依赖不要写进 inject,而是在使用点通过 ctx.get('serviceName') 查询并处理 undefined

6.5 Service 与 TypeScript 声明合并

服务是插件向其他插件公开的命名能力。运行时注册与编译时类型是两件事,必须同时完成:

复制代码
import { Service, type Context } from '@deepseek-ai/cordis'declare module '@deepseek-ai/cordis' {  interface Context {    counter: CounterService  }}export class CounterService extends Service {  private value = 0  constructor(ctx: Context) {    super(ctx, 'counter') // 运行时提供 ctx.counter  }  increment(): number {    return ++this.value  }}

declare module 是 TypeScript module augmentation:让编译器知道 ctx.counter 的类型;super(ctx, 'counter') 才是真正的运行时注册。只写前者会编译通过但运行时不存在,只写后者会运行但消费方没有类型。

6.6 类型事件与四种分发方式

事件用于松耦合通信。先扩展 Events

复制代码
declare module '@deepseek-ai/cordis' {  interface Events {    'demo/ready': (id: string) => void    'demo/check': (input: string) => string | undefined    'demo/transform': (      input: string,      next: () => Promise<string>,    ) => Promise<string>  }}

四种常用分发语义:

  1. ctx.emit(name, ...args):同步广播给所有监听器,忽略返回值,适合"事实已经发生"。
  2. ctx.bail(name, ...args):同步顺序查询,第一个不是 nullfalseundefined 的值结束分发,适合轻量匹配。
  3. await ctx.serial(name, ...args):异步顺序查询,第一个有效值结束后续处理,适合按优先级寻找处理者。
  4. await ctx.waterfall(name, input, terminal):中间件链,监听器通过 next() 委托下游,并可包装返回值。

Waterfall 最容易出错:

复制代码
ctx.on('demo/transform', async (_input, next) => {  const downstream = await next() // 观察或包装时必须调用  return downstream.trim()})

不调用 next() 表示故意短路,不是"监听完自动继续"。Harness 的 agent/pre-stepagent/requestllm/streamtools/pre-executetools/executetools/post-execute 都是 waterfall。

不要混淆 Cordis 实时事件与 Session Event:ctx.emit() 是进程内扩展点;turn/startassistant/messagetool/result 等是写入会话日志的耐久事实。观察后者应监听 session/event 并检查 event.type

6.7 Config Schema:类型、默认值和加载期校验

部署可变值不得硬编码。插件导出同名的 Config interface 与 Schemastery schema:

复制代码
import Schema from '@deepseek-ai/schemastery'export interface Config {  mode: 'upper' | 'lower'  timeoutMs: number}export const Config: Schema<Config> = Schema.object({  mode: Schema.union(['upper', 'lower']).default('upper'),  timeoutMs: Schema.number().default(30000),})export function apply(ctx: Context, config: Config): void {  if (!Number.isFinite(config.timeoutMs) || config.timeoutMs <= 0) {    throw new Error('timeoutMs must be a positive finite number')  }}

Schema 处理字段类型、枚举、必填项和默认值;正数、跨字段关系等 DSL 未表达的约束在最早可判断处显式校验。配置错误应阻止加载并给出修复方向,不能"使用一个猜测值继续"。

6.8 Profile、Bundle、Patch 与 Overlay

Profile 是 $DSH_HOME/profiles/<name> 下的用户组合,Bundle 是可分发的补丁层。最终树的顺序是:Bundle 列表 → Profile 自有 patch → $DSH_HOME/cordis.patch.yml → 命令行中的每个 --patch

后层按 row id 胜出。最关键的规则:patch 替换目标 row 的完整 config,不做深合并。覆盖 agent-default-model 时必须同时重述 providermodel。插入本地 TS 文件时使用绝对路径,因为 Loader 从 Profile 目录解析模块,不从 patch 文件目录解析。

先用 --dump-config 验证配置命中,再启动,是最省时的工作流。

6.9 Agent、Session、Turn 与 Step

  • •Agent:活跃驱动对象,有 inbox、状态、取消、注入和当前作用域 Context。
  • •Session:追加式事件日志,是持久事实源;Agent 可以释放,Session 仍可恢复。
  • •Turn:从一次输入被接纳开始,到没有后续工作时结束;可能没有模型请求,也可能包含多个 Step。
  • •Step:一次模型请求,加该响应触发的一组工具调用。
  • LLMSession LogAgent LoopUser/UIToolsLLMSession LogAgent LoopUser/UIlooptool callinbox messageturn/startagent/pre-stepstep/start + user/messagederiveMessages()agent/request → llm/streamassistant/chunk* → assistant/messagepre-execute → execute → post-executetool/resultstep/endturn

"模型可见即已记录"是硬性不变量。模型下一次请求看到的历史由 Session Log 的 deriveMessages() 投影得到;如果插件添加了模型可见上下文,却没有对应的日志事实,恢复、Fork、回放和遥测会与原请求不一致。

6.10 System Prompt、Tool Schema 与 LLM 请求

ctx.systemPrompt 聚合 persona、插件贡献的 section、动态 context 和工具 schema。Tool 注册变化会改变模型请求中的工具目录。LLM Adapter 接收提供方无关的 GenerateOptions,把系统提示、历史、工具、模型和取消信号转换为某个 HTTP/SDK 协议,再产生统一 StreamChunk

完整流至少遵循:每个 block-start 对应 block-end;文本使用 text-delta;工具参数使用原始 JSON 增量;usage 位于结束前;finish 是最后分片。写新 Adapter 时,还要传递 options.signal、应用归因 headers,并用稳定 code 的 LlmError 表达传输与协议失败。

多数 OpenAI-compatible 网关无需自己写 Adapter:配置 dsh-llm-pi-aiapi: openai-completions 即可。只有协议字段或流语义无法表达时,才实现新的 LlmAdapter

6.11 Tool 的五层契约

一个生产级 Tool 同时有五层:

  1. name/description:模型用来决定是否调用。
  2. parameters:模型参数 Schema;defineTool 在执行前校验并推导 TypeScript 类型。
  3. execute(args, exec):领域行为;args 已校验且按只读输入处理,长操作必须遵守 exec.signal
  4. output.schema:程序化规范值;Code Mode 得到的是这个值,不是人类文本。
  5. output.renderpresentCall/presentResult:前者生成模型可见 ContentBlock,后者生成 UI 卡片意图。

对象输出必须显式声明 additionalProperties: true | false。领域内的"不存在"可以是成功规范值;基础设施故障、取消和无法满足契约则抛异常。展示函数会在实时和日志回放时重复执行,所以必须是纯函数:不能做 I/O、读当前时间或随机数。

UI 卡片类型包括 genericterminaldiffsearchweb。工具不导入 React 或 Client 类型,只返回中性的 render intent。

6.12 Capability Seam:Definition、Provider、Consumer

当能力需要替换后端时,拆成三个角色:

复制代码
Service Definition ← Provider        ↑     Consumer/Tool
  • •Definition 拥有接口、Request/Result 类型和 ctx key。
  • •Provider 只依赖 Definition,实现本地、沙箱或远端行为。
  • •Consumer 也只依赖 Definition,把能力提供给模型、CLI 或其他服务。

Provider 与 Consumer 互不依赖,替换 Provider 不改 Tool。简单工具不必预防性拆包;只有角色确实独立演进时才拆分。

6.13 Host、Client、Typert 与 Web

Node Host 拥有 Agent、文件系统、模型和持久化;Browser Client 只通过 API 操作 Host 并渲染 Session Event。两边使用独立 TypeScript aggregate,因为它们会对相同 Context key 做不同声明合并。Host 构建中的 Typert 扫描 @Remote/@RemoteScope,生成运行时反射和 Client Remote 投影;浏览器通过 ctx.remote 调用,而不是直接 import Host 实现。

apps/web 只是 Vite 入口;真正 UI 功能来自 packages/client/ui-* 插件。想增加业务消息展示,优先注册 Conversation Node;想增加后端能力,写 Host 插件和 Remote;不要把业务逻辑塞进 React 组件。

6.14 权限、持久化、凭据和遥测

默认新会话使用 workspace-write + ask:写操作被限制并可能审批,但读取、网络、同 UID 文件和进程可见性不是完整机密隔离。danger-full-access 不审批,只能用于可丢弃环境。

Session 默认持久化到 $DSH_HOME/sessions 的 JSONL。Credential 文档默认是 $DSH_HOME/.credentials.yaml,权限 0600;同 UID 的 Agent 工具理论上仍可读取它,因此文件权限不是抵御自身 Agent 的安全边界。

遥测默认关闭。显式开启 FULLFEEDBACK_ONLY 可能上传消息、工具参数/结果和路径;任何非空 DSH_TELEMETRY_DISABLED 都会强制关闭。

7. 本机自定义模型:地址、模型切换与启动实例

7.1 已实测的网关事实

  • •用户提供地址:http://x.x.x.x:3000/
  • •根路径 /models 返回 HTML 管理页面,不是模型 JSON。
  • •OpenAI-compatible API 前缀:http://x.x.x.x:3000/v1
  • GET /v1/models 携带 Bearer Key 返回模型目录。
  • POST /v1/chat/completions 使用 deepseek-v4-flash 返回 HTTP 200;服务实际报告模型 deepseek-v4-flash-202605
  • •最小验证回复为 DSH_ENDPOINT_OK

7.2 完整自定义 Provider overlay

文件:.artifacts/deepseek-harness-learning-lab/custom-model/cordis.yml。完整内容如下;Key 不写入 YAML,只引用 DEEPSEEK_API_KEY

复制代码
# 自定义 OpenAI-compatible 网关。切换地址或模型时,只改 baseURL 与 models[0].id,# 并同步修改 agent-default-model.model。API Key 只从 DEEPSEEK_API_KEY 解析。- id: llm-pi-ai  config:    providers:      learning-gateway:        displayName: '学习用自定义网关'        apiKeyEnv: DEEPSEEK_API_KEY        api: openai-completions        baseURL: 'http://x.x.x.x:3000/v1'        defaultContextWindow: 131072        defaultMaxTokens: 8192        models:          - id: deepseek-v4-flash            name: DeepSeek V4 Flash(自定义网关)            contextWindow: 131072            maxTokens: 8192- id: agent-default-model  config:    provider: learning-gateway    model: deepseek-v4-flash

contextWindowmaxTokens 是本学习实例的部署声明,不是从 /models 自动获得的权威容量;网关限制不同就按实际值修改。models 是该 route 的允许目录,列多个模型即可在 Web 模型选择器切换,例如:

复制代码
models:  - id: deepseek-v4-flash    name: DeepSeek V4 Flash  - id: deepseek-v4-pro    name: DeepSeek V4 Pro  - id: deepseek-chat    name: DeepSeek Chat  - id: deepseek-reasoner    name: DeepSeek Reasoner

切换默认模型要同步修改 agent-default-model.model。Web 中临时切换可点击输入框右下角模型按钮;已开始请求的 Session 会保留日志中的模型身份,新默认只影响之后创建或明确切换的请求。

7.3 一条命令启动完整学习实例

仓库根 .env 已含你的私有 DEEPSEEK_API_KEY。启动命令是:

复制代码
pnpm dsh web \  --patch ./.artifacts/deepseek-harness-learning-lab/custom-model/cordis.yml \  --patch ./.artifacts/deepseek-harness-learning-lab/greet-plugin/cordis.yml \  --patch ./.artifacts/deepseek-harness-learning-lab/text-transform-plugin/cordis.yml

当前实例已运行在 http://127.0.0.1:3080。验证顺序:

  1. •打开地址;设置 → 模型应出现"学习用自定义网关",并显示 Key 已配置。
  2. •选择工作区 /root/deepseek-ai/deepseek-harness
  3. •输入框右下角应显示"DeepSeek V4 Flash(自定义网关)"。
  4. •设置 → 插件 → 插件列表应看到 greet,以及 text-transform 的 provider/tool/audit,均为已启用。
  5. •发送:请务必依次调用 greet 工具问候 Ada,再调用 transform_text 工具转换文本 DeepSeek Harness。不要使用其他工具,最后只总结两个工具的结果。
  6. •预期:你好,Ada!RESULT: DEEPSEEK HARNESS

模型页中的自定义 Provider(绿色圆点表示凭据已配置,页面不会回显明文 Key):

插件列表筛选 learning-lab 后,可看到简单 Demo 与复杂 Demo 的四个模块均已启用:

本机真实结果:1 个 Turn、2 个 Step,两个 Tool Call 均成功,最终回答在约 5 秒内完成。终端的 audit 插件打印 [text-transform] mode=upper input="DeepSeek Harness"

7.4 换地址或协议

  • •同类 OpenAI Chat Completions 网关:修改 baseURL,保留 api: openai-completions
  • •地址必须包含服务实际 API 前缀;本例必须有 /v1
  • •Key 变量名可改为 MY_GATEWAY_API_KEY,但必须同步改 apiKeyEnv 并通过环境、Credentials 页或 0600 credential 文件提供。
  • DEEPSEEK_BASE_URL 只用于官方 dsh-llm-deepseek route,且是 bootstrap-only 变量;本例使用 pi-ai 手工 route,不需要它。
  • •网关不支持 GET /models 时可手写 models,不影响 Chat Completions。

8. 简单 Demo:单文件可配置 greet Tool

8.1 目录

复制代码
greet-plugin/├── cordis.yml├── tsconfig.json└── src/index.ts

8.2 完整 TypeScript 源码

复制代码
import type { Context } from '@deepseek-ai/cordis'import Schema from '@deepseek-ai/schemastery'import { defineTool } from '@deepseek-ai/dsh-tools'export const name = 'learning-greet-tool'export const inject = ['tools']export interface Config {  greeting: string  punctuation: string}export const Config: Schema<Config> = Schema.object({  greeting: Schema.string().default('你好'),  punctuation: Schema.string().default('!'),})export function apply(ctx: Context, config: Config): void {  ctx.tools.register(defineTool({    name: 'greet',    description: '使用配置好的问候语向指定的人问好。',    parameters: {      name: { type: 'string', required: true, description: '要问候的人名' },    },    output: {      schema: { type: 'string' },      render: (_args, value) => [{ type: 'text', text: value }],    },    async execute(args) {      return `${config.greeting},${args.name}${config.punctuation}`    },  }))}

8.3 Overlay 完整代码

复制代码
- insert:    - id: learning-greet-tool      name: '/root/deepseek-ai/deepseek-harness/.artifacts/deepseek-harness-learning-lab/greet-plugin/src/index.ts'      config:        greeting: '你好'        punctuation: '!'

8.4 逐段解释

  • name 是插件诊断名;Tool 自己的模型可见名字是 greet,两者不是同一命名空间。
  • inject = ['tools'] 使 Fiber 在 ctx.tools 可用后才加载。
  • Config interface 给 TypeScript 使用,同名 Schema 给 Loader 运行时校验并填默认值。
  • parameters 生成模型看到的 JSON Schema;required: trueargs.name 成为必填字符串。
  • execute 返回规范值,本例规范值就是字符串。
  • output.schema 验证返回值;返回数字会被转成 Tool 错误,而不是悄悄 stringify。
  • render 将规范值转成模型可见的 ContentBlock。UI 若没有专用卡片,会使用 generic fallback。
  • •注册由当前 Fiber 所有,修改 overlay 触发热替换时旧 Tool 自动注销。

8.5 练习与验收

  1. •把 greeting 改成 欢迎,重启或让配置热替换,调用 greet
  2. •增加可选参数 title;未提供时只用姓名,提供时输出"你好,Dr. Ada!"。
  3. •把输出改成对象 { message, target },必须增加对象 schema 和 additionalProperties: false
  4. •故意返回错误类型,观察 Tool Result 的 isError,再修复。

验收标准:--dump-config 能找到 row;设置插件列表显示已启用;模型明确调用后返回配置文本;改配置不会出现重复 Tool 注册。

9. 复杂 Demo:Definition → Provider → Consumer → Event

9.0 text-transform-plugin 到底是做什么的

先说结论:text-transform-plugin 是本教材编写的教学插件,不是 DeepSeek Harness 内置的产品功能。它本身的能力很小------把一个字符串按配置转成大写或小写、可加前缀、带一个 30 秒自动清空的缓存------但它的价值不在于"转换文本",而在于用四段代码把一个完整**能力 seam(Definition → Provider → Consumer → Event)**的协作关系演出来,作为第 6.12 节那套概念的可运行样板。

  • •它是给学习者看的:真实项目里"稳定接口"和"可替换实现"怎么拆、运行时注册和编译时类型怎么配对、事件怎么跨模块松耦合。
  • •它不是给产品用的:没有任何一条产品需求需要"把文本转大写"这个工具,所以它不会也不应该被合并进仓库、发布到 npm。
  • •它当前没有安装进 Profile:只是通过第 7.3 节启动命令末尾的 --patch text-transform-plugin/cordis.yml 临时加载。重启时去掉那一行 --patch,它就消失了,无需也不应执行 pnpm uninstall

四段代码分别扮演四个角色,一次运行就能看到整条链:

复制代码
模型(browser) → transform_text Tool(tool.ts, Consumer)              → ctx.textTransform(provider.ts, Provider)              → 本地实现 + 缓存 + 清理              → emit 'learning-text/transformed'              → audit.ts 监听并打印终端日志

也就是说,浏览器里点一次"转换文本",终端会同时打印 [text-transform] mode=upper input="DeepSeek Harness"。这条链路覆盖了本教材第 6 章的 injectService、声明合并、ctx.plugin 子 Fiber、ctx.effect 清理、类型事件广播,是后续替换成 HTTP Provider(第 9.6 节)的起点。

复制代码
text-transform-plugin/├── cordis.yml├── tsconfig.json└── src/    ├── service.ts   # Service Definition + Request/Result + typed event    ├── provider.ts  # Local Provider + Config + cache + cleanup    ├── tool.ts      # Consumer Tool + canonical output + UI intent    └── audit.ts     # Event observer

9.1 service.ts:稳定能力接口

复制代码
import { Service, type Context } from '@deepseek-ai/cordis'export interface TransformRequest {  text: string}export interface TransformResult {  input: string  output: string  mode: 'upper' | 'lower'}declare module '@deepseek-ai/cordis' {  interface Context {    textTransform: TextTransformService  }  interface Events {    'learning-text/transformed': (result: TransformResult) => void  }}/** Service Definition:定义调用者依赖的稳定能力,不决定能力如何实现。 */export abstract class TextTransformService extends Service {  constructor(ctx: Context) {    super(ctx, 'textTransform')  }  abstract transform(request: TransformRequest): Promise<TransformResult>}

Definition 拥有 TransformRequest/TransformResult,因此 Provider 与 Consumer 对同一契约编译。抽象类注册 ctx.textTransform 的运行时名字;module augmentation 提供编译时类型。事件表示"转换已发生",所以使用 emit 广播,不返回决策。

9.2 provider.ts:本地实现、配置、缓存与清理

复制代码
import type { Context } from '@deepseek-ai/cordis'import Schema from '@deepseek-ai/schemastery'import {  TextTransformService,  type TransformRequest,  type TransformResult,} from './service.ts'export interface Config {  mode: 'upper' | 'lower'  prefix: string  cleanupIntervalMs: number}export const Config: Schema<Config> = Schema.object({  mode: Schema.union(['upper', 'lower']).default('upper'),  prefix: Schema.string().default(''),  cleanupIntervalMs: Schema.number().default(30000),})class LocalTextTransformService extends TextTransformService {  private readonly cache = new Map<string, TransformResult>()  private readonly mode: Config['mode']  private readonly prefix: string  constructor(ctx: Context, config: Config) {    super(ctx)    this.mode = config.mode    this.prefix = config.prefix    const timer = setInterval(() => this.cache.clear(), config.cleanupIntervalMs)    this.ctx.effect(() => () => clearInterval(timer))  }  async transform(request: TransformRequest): Promise<TransformResult> {    const cached = this.cache.get(request.text)    if (cached !== undefined) return cached    const transformed = this.mode === 'upper'      ? request.text.toUpperCase()      : request.text.toLowerCase()    const result: TransformResult = {      input: request.text,      output: `${this.prefix}${transformed}`,      mode: this.mode,    }    this.cache.set(request.text, result)    this.ctx.emit('learning-text/transformed', result)    return result  }}export const name = 'learning-text-transform-provider'export function apply(ctx: Context, config: Config): void {  if (!Number.isFinite(config.cleanupIntervalMs) || config.cleanupIntervalMs <= 0) {    throw new Error('cleanupIntervalMs must be a positive finite number')  }  ctx.plugin(LocalTextTransformService, config)}

外层函数插件负责配置 Schema,内层 Service 类负责状态。ctx.plugin(LocalTextTransformService, config) 创建子 Fiber;父插件卸载会递归卸载 Service。Timer 是自有资源,因此放入 Effect。缓存命中不再次 emit,事件语义是"完成了一次实际转换",不是"每次 API 调用"。

9.3 tool.ts:模型 Consumer 与结构化输出

复制代码
import type { Context } from '@deepseek-ai/cordis'import { defineTool } from '@deepseek-ai/dsh-tools'import './service.ts'export const name = 'learning-text-transform-tool'export const inject = ['tools', 'textTransform']export function apply(ctx: Context): void {  ctx.tools.register(defineTool({    name: 'transform_text',    description: 'Transform text with the configured text transformation provider.',    parameters: {      text: { type: 'string', required: true, description: 'Text to transform' },    },    output: {      schema: {        type: 'object',        additionalProperties: false,        properties: {          input: { type: 'string', required: true },          output: { type: 'string', required: true },          mode: { type: 'string', required: true },        },      },      render: (_args, value) => [{        type: 'text',        text: `Mode: ${value.mode}\nInput: ${value.input}\nOutput: ${value.output}`,      }],    },    presentCall: args => ({      card: 'generic',      title: 'Transform text',      rawInput: args.text,    }),    async execute(args, exec) {      if (exec.signal.aborted) throw exec.signal.reason      if (args.text.trim() === '') throw new Error('text must not be blank')      return ctx.textTransform.transform({ text: args.text })    },  }))}

Consumer 只依赖 Service Definition,不知道本地缓存如何实现。Schema DSL 能保证字符串类型,但不能表达"trim 后非空",所以在 execute 最早检查。exec.signal 已中止时立即失败;真实网络 Provider 还应把该 signal 传给 fetch。规范对象适合 Code Mode,render 文本适合模型,presentCall 只决定 UI pending 卡片。

9.4 audit.ts:类型事件观察者

复制代码
import type { Context } from '@deepseek-ai/cordis'import './service.ts'export const name = 'learning-text-transform-audit'export function apply(ctx: Context): void {  ctx.on('learning-text/transformed', (result) => {    console.log(`[text-transform] mode=${result.mode} input=${JSON.stringify(result.input)}`)  })}

ctx.on() 的参数由 Events 声明自动推导。监听器随 audit Fiber 自动移除,不需要 off()。这个事件只写终端日志,不是 Session Event,也不会进入模型上下文。

9.5 cordis.yml:组合三个角色

复制代码
- insert:    - id: learning-text-transform-provider      name: '/root/deepseek-ai/deepseek-harness/.artifacts/deepseek-harness-learning-lab/text-transform-plugin/src/provider.ts'      config:        mode: upper        prefix: 'RESULT: '        cleanupIntervalMs: 30000    - id: learning-text-transform-tool      name: '/root/deepseek-ai/deepseek-harness/.artifacts/deepseek-harness-learning-lab/text-transform-plugin/src/tool.ts'    - id: learning-text-transform-audit      name: '/root/deepseek-ai/deepseek-harness/.artifacts/deepseek-harness-learning-lab/text-transform-plugin/src/audit.ts'

YAML 顺序不是依赖关系:Tool 的 inject 才保证 Provider 已提供 textTransform。把 mode 改成 lower,Provider Fiber 会被替换,Tool 因必需服务短暂消失而自动重载。

9.6 如何把 Provider 换成 HTTP

保留 service.tstool.ts,只新增另一个 Provider:

复制代码
class HttpTextTransformService extends TextTransformService {  constructor(ctx: Context, private readonly endpoint: string) {    super(ctx)  }  async transform(request: TransformRequest): Promise<TransformResult> {    const response = await fetch(this.endpoint, {      method: 'POST',      headers: { 'content-type': 'application/json' },      body: JSON.stringify(request),    })    if (!response.ok) throw new Error(`transform endpoint returned ${response.status}`)    return response.json() as Promise<TransformResult>  }}

生产版本应继续加入超时/调用方 signal、响应边界校验、凭据引用和稳定错误分类。关键收益是 Tool 完全不改,模型契约也不改。

9.7 复杂 Demo 的练习与验收

  1. •将 mode 改成 lower,预期 RESULT: deepseek harness
  2. •连续两次转换同一文本,观察 audit 只在首次实际计算时打印。
  3. •删除 Provider row,Tool 应因缺少 textTransform 进入 PENDING/卸载,而不是调用 undefined。
  4. •写 ReverseTextTransformService 并替换 Provider,不改 Tool。
  5. •新增 learning-text/before-transform waterfall,允许策略拒绝超长输入;有意拒绝时不调用 next(),普通观察时必须调用。

验收标准:TypeScript 检查通过;--dump-config 有三行;插件列表全启用;真实模型能调用;配置替换后没有残留 timer;替换 Provider 后 Tool Schema 不变。

10. Tool 开发方法全集

10.1 参数 Schema

标量支持 string、number、integer、boolean、null;复杂参数可使用 array、object、联合和字面量约束。隐式 parameters 根对象由 Tool DSL 管理;显式 object 节点必须声明 additionalProperties。Schema 负责 JSON 结构,业务规则仍在 execute 校验。

10.2 规范值与模型文本分离

错误做法是 execute 返回"job started: abc-1",然后另一个 Tool 从文本解析 id。正确做法是返回 { kind: 'background', jobId: 'abc-1' },render 再生成人类文本。这样 Code Mode、UI 和测试都使用稳定字段。

10.3 取消与长任务

前台任务把 exec.signal 传到 fetch、文件 API 或子进程。后台任务使用 ctx.jobs.start() 后由 Job 自己的取消信号拥有生命周期;外层 Tool Call 结束不应杀死已经发布的后台 Job。

10.4 策略与观察

  • tools/pre-execute:允许、拒绝、审批;ctx.tools.guard() 可设置后续无法撤销的最终拒绝。
  • tools/execute:around-dispatch,可加超时、重试和指标。
  • tools/post-execute:修改模型展示、阻止结果或附加上下文。
  • tools/result:只观察最终不可变结果。

不要把所有部署策略写死在每个 Tool 内;策略插件可以统一覆盖整条流水线。

10.5 UI Render Intent

  • •读取或普通动作:generic,可附 locations
  • •Shell 命令:terminal
  • •文件变更:diff,回放需要的旧/新文本应来自持久 meta。
  • •Glob/Grep:search
  • •搜索/抓取:web

presentCall/presentResult 必须是纯函数。实时执行与历史回放都调用它们,任何 I/O 都会让同一 Session 显示不一致。

10.6 Code Mode

已注册工具自动以 await tools.<name>(args) 暴露给 Code Mode,无需再写适配器。成功值是 output.schema 对应的规范 JSON;失败抛 ToolCallError。因此 Schema 也是程序化 API,字段命名要稳定明确。

11. 插件发布:从本地 Overlay 到可安装 Bundle

本地实验使用绝对路径;交给别人应创建 npm Bundle:

复制代码
dsh-text-transform-bundle/├── package.json├── cordis.patch.yml└── lib/    ├── service.js    ├── provider.js    ├── tool.js    └── audit.js

package.json

复制代码
{  "name": "dsh-text-transform-bundle",  "version": "0.1.0",  "type": "module",  "files": ["lib", "cordis.patch.yml"],  "dsh": {    "bundle": {      "patch": "./cordis.patch.yml"    }  }}

Bundle 中的 row 使用包 exports,不使用开发机绝对路径。安装:

复制代码
dsh plugin --profile web add dsh-text-transform-bundledsh --profile web --dump-configdsh web

从 Git 安装源码包时,作者必须提供自包含 prepare 构建;pnpm 10+ 要求用户在 Profile 的 pnpm-workspace.yaml 明确 allowBuilds。这等同授权依赖在 Agent 沙箱外执行安装脚本,只对可信且锁定 commit 的仓库授权。npm 预构建包或 pnpm pack tarball 不需要安装期构建权限。

11.1 DeepSeek Harness 插件怎么安装

"插件"在这里有三条完全不同的安装路径,先分清各自适用场景,避免用错命令:

路径 命令 何时用 卸载方式
临时 Overlay dsh web --patch ./xxx/cordis.yml 本地开发、学习、调试,每次启动临时加载 重启时去掉该 --patch,什么都不用删
正式 npm Bundle dsh plugin --profile web add <包名> 分发可复用的第三方插件/Bundle dsh plugin --profile web remove <包名>
源码目录 / Git dsh plugin --profile web add ./本地目录<git url> 尚未发布到 npm 的源码 checkout dsh plugin --profile web remove <包名>

路径一:临时 Overlay(本教材两个 Demo 用的就是这条)。 --patch 只是把一份 cordis.yml 作为最末一层覆盖到启动树,不写入任何 Profile 文件,不创建 node_modules 依赖。greet-plugintext-transform-plugin 都是用绝对路径 name 直接指向 .ts 源码,由源码 CLI 的 TSX 现场执行,所以根本谈不上"卸载":重启时不带这一行就消失。

路径二/三:正式 Bundle(dsh plugin)。 这是真正的"安装"。Bundle 是带 dsh.bundle.patch 清单(见上文 package.json)的 npm 包,add 之后它成为 Profile 目录里 pnpm-workspace.yaml 声明的依赖,随 dsh --profile web 每次启动稳定加载:

复制代码
# 从 npm 安装已发布包dsh plugin --profile web add dsh-text-transform-bundle# 从本地目录安装(相对路径以调用目录为锚点)dsh plugin --profile web add ./dsh-text-transform-bundle# 从 Git 仓库安装(需作者提供 prepare 构建,见上文 allowBuilds)dsh plugin --profile web add https://github.com/you/dsh-text-transform-bundle.git

11.2 查看已装插件与状态

安装前后都先"看"再"动",三处互相印证:

复制代码
# 1. 看最终组合树里有哪些 row、是否命中你的覆盖dsh --profile web --dump-config# 2. 看 Profile 目录里实际声明了哪些插件依赖dsh plugin --profile web list   # 若当前版本提供;否则看 $DSH_HOME/profiles/web/pnpm-workspace.yaml# 3. 运行时看每个 Fiber 是 ACTIVE / PENDING / FAILED#    浏览器打开 Web UI → 设置 → 插件 → 插件列表

浏览器插件列表是最直观的一处:本教材四个 Demo 模块(learning-greet-toollearning-text-transform-provider/tool/audit)都显示"已启用";若某个模块停在 PENDING,通常是它的 inject 依赖(如 textTransform)没有加载。

11.3 禁用、升级与彻底卸载

临时 Overlay:

  • •禁用某一行:在该 cordis.yml 的 row 上写 disabled: true(保留 row 便于恢复),或直接删掉该 row。
  • •彻底卸载:重启时从命令中去掉对应的 --patch。示例中的 text-transform-plugin 从未写进 Profile,因此"卸载"就是重启时不带它,不要执行 pnpm uninstalldsh plugin remove

正式 Bundle:

复制代码
# 升级到新版本dsh plugin --profile web add dsh-text-transform-bundle@latest# 彻底卸载:移除依赖并在 Profile 中清除该 patch 层dsh plugin --profile web remove dsh-text-transform-bundle# 卸载后确认 row 已消失dsh --profile web --dump-config

remove 会从 Profile 的依赖清单中移除该包并撤销其补丁层;它操作的是 Profile 的插件清单,不是仓库根 package.json,所以和 pnpm remove 是两回事。卸载后如仍能在 --dump-config 看到该 row,说明它还来自 Profile 自有 patch、$DSH_HOME/cordis.patch.yml 或某个 --patch,逐层排查而不是重复执行 remove。

12. 具体学习路线:每一步学什么、做什么、如何验收

第 1 阶段:产品使用与数据流(约 1 小时)

学习内容:Provider 是连接路由,Model 是路由内身份,Workspace 是工具默认文件根,Permission 决定写操作与审批,Session 是耐久日志,Agent 是当前驱动。

动手:启动第 7.3 节实例;查看设置的模型与插件;选择 workspace;发送普通问答;打开 Session log;找到 turn/startrequest/headerassistant/messageturn/end

验收:能解释为什么关闭浏览器不会删除 Session,为什么切换默认模型不一定修改已有 Session,为什么未选择 workspace 时输入框禁用。

第 2 阶段:组合系统(约 1 小时)

学习内容:Profile 是用户组合,Bundle 是发布层,Patch 按 row id 覆盖,Overlay 是临时最后层,配置整行替换。

动手:分别 dump 默认 Web、加入 custom-model、再加入两个 Demo;搜索 agent-default-modelllm-pi-ailearning-greet-tool

验收:能画出层顺序;能预测交换两个 --patch 的结果;能说明为什么覆盖 row 时不能只写一个 config 字段。

第 3 阶段:第一个 Tool(约 1 小时)

学习内容:Plugin/Fiber/inject/Config/Tool parameters/execute/output/render。

动手:逐字重建第 8 章代码;增加 title 可选参数;把输出从 string 改为 object;故意制造 Schema 不匹配再观察错误。

验收:插件列表已启用;模型能调用;Code Mode 可获得结构化值;热替换不产生重复注册。

第 4 阶段:生命周期与事件(约 1.5 小时)

学习内容:Effect 自动清理、子 Fiber、服务消失导致消费方卸载、emit/bail/serial/waterfall。

动手:为 greet 增加计数 Service;每次执行 emit;增加 observer;增加 timer 并通过 Effect 清理;增加 waterfall 将名字 trim。

验收:卸载插件后 timer 停止;普通 waterfall listener 调用 next;故意短路时下游不运行;事件参数有类型推导。

第 5 阶段:完整能力 Seam(约 2 小时)

学习内容:Definition 拥有类型,Provider 和 Consumer 互不依赖,运行时 Service 与编译时 module augmentation 缺一不可。

动手:完整重建第 9 章;写 reverse Provider;再写 HTTP Provider;只改 overlay 切换实现。

验收:Tool 源码完全不变;Provider 切换后结果改变;缺 Provider 时 Tool 不加载;审计事件只记录实际转换。

第 6 阶段:Agent Loop 与持久化(约 2 小时)

学习内容:Turn/Step、三段 Tool Pipeline、流式 Chunk、Session Event 投影、"模型可见即记录"。

动手:发送同时调用两个 Tool 的提示;在轨迹页确认 1 Turn/2 Steps;导出 Session;从日志手工定位两个 tool/calltool/result

验收:能从日志解释下一次模型请求的历史;能说明 Cordis audit event 为什么不会进入 Session;能指出新增模型上下文时为何要新增耐久事件。

第 7 阶段:模型与 Web 扩展(半天)

学习内容:pi-ai route、手写 LLM Adapter 的适用边界、Host/Client、Typert Remote、Conversation Node、UI Tool intent。

动手:在 custom route 增加第二个模型并从 UI 切换;观察 request/header;给 Tool 增加 generic locations;阅读一条生成 Remote 的 Host 方法和 Client 调用。

验收:能判断一个需求应放 Adapter、Provider、Tool、Remote 还是 Client UI;能说明为什么业务后端不应写进 React。

13. 测试、验证与调试

13.1 本教材已执行的验证

  • pnpm install 成功;原生 node-pty 编译成功。
  • pnpm run build 成功。
  • pnpm run typecheck 成功。
  • •两个 Demo 独立 TypeScript 检查成功。
  • •CLI 与 base/web-app/headless Bundle:4 个文件、25 项测试通过。
  • •自定义网关 /v1/models/v1/chat/completions 均 HTTP 200。
  • •Web 模型页确认 learning-gateway 与 Key 已配置。
  • •四个 Demo 模块均在插件列表显示已启用。
  • •真实模型依次调用两个 Tool,结果符合预期。

13.2 开发时最短检查链

  1. pnpm dsh web --patch ... --dump-config:配置是否命中。
  2. pnpm exec tsc -p <demo>/tsconfig.json:类型是否满足公开声明。
  3. •启动 Web,设置 → 插件:Fiber 是否挂载。
  4. •使用明确提示强制调用:区分"模型选择不调用"和"Tool 未注册"。
  5. •查看 Session log/轨迹:参数、结果、Step 是否正确。
  6. •修改配置,验证旧注册、timer、socket 是否清理。

13.3 仓库命令

命令 用途
pnpm run build Host lib → Client lib → Web 前端
pnpm run typecheck 严格 TypeScript 与 Typert 生成契约
pnpm run lint Oxlint
pnpm run test 默认 Vitest
pnpm run test:coverage CI 的逐文件覆盖率门禁
pnpm run test:e2e 真实 API;无 Key 的相关项自跳过
pnpm run test:snapshot Keyless 模型响应回放
pnpm run test:web 构建后的浏览器测试
pnpm run doc-sync 文档生成、新鲜度、链接与配对
pnpm run hygiene knip、publint、约束与运行闭包

13.4 常见故障

  • •Web 可开但不能输入:先添加并选择 workspace,再确认 Model。
  • MISSING_CREDENTIAL:Key 未被 route 的 apiKeyEnv 解析;重启后检查高优先级启动环境和 Credentials 页面。
  • •根 /models 是 HTML:尝试端点实际 API 前缀;本例是 /v1/models
  • UNKNOWN_MODEL:模型不在 pi-ai route 的 models 中,或模型 id 拼错。
  • •401/403:Key 无效或未传;不要打印 Key 调试,只看来源描述符和 HTTP 状态。
  • •404 Chat Completions:baseURL/v1,或网关不是 openai-completions
  • •Overlay 改一个字段导致其他字段消失:目标 config 是整体替换。
  • •插件 PENDING:inject 的服务不存在;检查服务运行时名字与 module augmentation 是否一致。
  • •Tool 注册但不调用:明确要求调用,检查模型是否支持 Tool Calling,再看请求 Tool Schema。
  • .env 修改无效:启动快照已冻结,需要重启;bootstrap-only 变量根本不允许在文件层。
  • •Credential 权限错误:POSIX 上执行 chmod 600 ~/.dsh/.credentials.yaml
  • •源码 CLI 缺 Typert/Client bundle:先运行 pnpm run build
  • node-pty 下载头文件失败:NVM 环境可把 npm_config_nodedir 指向当前精确 Node 版本安装目录后重试。

14. 初学者最容易犯的设计错误

  1. •靠 YAML 顺序代替 inject:加载在热替换或隔离场景下会随机失败。
  2. •在 waterfall 观察者中忘记 next():整个请求链被短路。
  3. •注册原生 listener/timer 却不用 Effect:HMR 后重复执行并泄漏。
  4. •在 Service constructor 启动不受管理的异步任务:服务已可见但初始化未完成。
  5. •只有 module augmentation,没有 super(ctx, key):编译有属性,运行时没有服务。
  6. •只有运行时服务,没有声明合并:消费方失去类型安全。
  7. •Tool 返回自然语言句柄:Code Mode 和其他 Tool 只能脆弱解析文本。
  8. •对象 output 不写 additionalProperties:契约不明确或定义被拒。
  9. •忽略 exec.signal:取消 Agent 后网络或进程仍在运行。
  10. •在 presentCall 做 I/O:历史回放与实时 UI 不一致。
  11. •把模型可见信息仅塞进内存:恢复后请求无法重建。
  12. •把 Provider 和 Consumer 相互 import:无法替换后端,依赖图形成环。
  13. •把 Key 写进 YAML 或提交:凭据进入配置、日志和 Git 历史。
  14. •把网关主页当 API Base URL:/models 得到 HTML,Chat 请求 404。
  15. •把模型容量示例当权威:上下文和输出上限应按部署事实配置。

15. 其他运行形态

Headless

复制代码
pnpm dsh --profile headless "summarize this workspace"

它创建一个新持久 Session,运行到空闲,打印最后非空 Assistant 文本后退出;没有 HTTP 或浏览器。自定义模型可同样通过 --patch custom-model/cordis.yml 覆盖。

Python SDK

复制代码
from pathlib import Pathfrom deepseek_harness import DeepSeekHarnesswith DeepSeekHarness(    provider="deepseek-official",    model="deepseek-v4-flash",    cwd=str(Path("/tmp/disposable-workspace").resolve()),    session_root=str(Path("/tmp/dsh-sessions").resolve()),    cordis=str(Path("examples/jsonrpc-agent/minimal.cordis.yml").resolve()),) as harness:    result = harness.run("Inspect this workspace", session_id="study-001")print(result.final_response)

Minimal 示例使用 danger-full-access 和持久 PTY,只在可丢弃 checkout 或容器运行。独立任务使用不同 Session ID;复用 ID 会延续会话和 Shell 状态。

ACP 与 JSON-RPC

pnpm run demo:acp 通过 stdio 提供 Agent Client Protocol;packages/sdk/{protocol,server,client} 提供通用 JSON-RPC。自动化程序通过协议创建 Session、调用 Turn、接收通知和取消,而不是解析 CLI 文本。

MCP、Schedule、Workflow 与 Subagent

  • •MCP:dsh-mcp-client 连接外部工具服务器;server 命令是部署可信代码,不在 Agent 沙箱内。
  • •Schedule:examples/web-schedule 增加 Session 内持久提醒。
  • •Workflow:模型编写的 Worker Thread 工作流,通过 workflow/ralph Tool 执行。
  • •Subagent:Provider 可以是进程内 spawn/fork、ACP、Claude Code、Codex 或 DSH SDK;模型 Consumer 仍使用统一 subagent Tool。

16. 单文件之外的权威来源

这份文件已经内联快速掌握所需知识;遇到版本变化时按以下所有权查证:架构看 docs/architecture.zh.md,类型与服务方法看 docs/subsystems/,全部 Tool/Config 看生成的 docs/tool-catalog.mddocs/config-catalog.md,单包语义看对应 README,设计理由看 .agents/notes/implemented/,机器实际组合看 dsh --dump-config。不要用旧教程替代当前源码和生成目录。

相关推荐
suaizai_1 天前
从零开发一个 DeepSeek Harness 插件
harnes
zzz_23682 天前
【AI代码测评】OpenCodeReview 架构拆解:确定性工程与 Agent 如何分工
人工智能·架构·agent·agent测评·harnes