DeepSeek Harness Hub
← 返回列表

插件式智能体内核SheltonLiu-N/nano-cordis

DeepSeek Harnessspec-screened在 GitHub 查看 ↗
✓ 可直接安装

用约 1600 行代码读懂插件驱动的智能体运行机制

自动检查通过:npm 包已发布且 engines 声明满足基线(声明 Node >=20);该结论来自程序自动检查,未经人工实机验证。 · 最近上游提交 2026/8/18 · 已提供中文文档

Cordis 和 DeepSeek Harness 的纳米级重新实现,一个由插件构建的 AI 智能体运行时,小到可以在一个下午读完。

综合分
36.3
GitHub 分
36.3
用户评分
★ Stars
9
周下载量
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add nano-cordis
npm 包 nano-cordis 已校验归属本仓库,走 npm 安装最省事
数据截至 2026/9/15(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
安装兼容性检查✓ 自动检查通过

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

npm 包nano-cordis @ 0.1.0
Node 引擎要求 >=20 · 基线 Node 22.19 满足
dsh CLI 依赖未声明 dsh 版本约束
入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后验证 2026/9/17 18:24:14

用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动

README

NanoCordis 是一个规模很小、可以从头读到尾的代码库,它回答一个问题:如何用插件构建出一个 AI 智能体运行框架?它包含两层,合计约 1600 行 TypeScript:

- src/cordis/:对 Cordis 插件框架的最小化重新实现,保留其全部核心思想——插件、服务、依赖驱动的加载、effect(可撤销的注册)、事件、配置驱动的组合、热更新——并去掉其余部分。
- src/harness/:建立在前者之上的最小智能体运行框架,结构仿照 DeepSeek Harness:一份作为唯一数据源的会话日志、一个“询问模型并执行其工具调用”的循环、一条工具流水线、一个审批策略、一个命令行界面——每一项都是插件。

一个下午就能读完全部代码。凡真实项目在代码某个具体位置做得更多的地方,都留有一条简短的 // Omitted:(省略了什么)或 // Differs:(有何不同)注释,说明完整版的做法与原因;整体缺失的子系统则统一列在本文末尾。无论哪种情况,你始终知道完整版在何处更进一步。

为什么要有这个项目

智能体运行框架(agent harness)是围绕语言模型运行的那个程序:它保存对话、决定模型看到什么、执行模型要求的工具、与用户交流。这类程序最终都会面对同一个矛盾:它需要许多可替换的部件(模型、工具、存储、界面、策略),而这些部件又要在互不硬编码的前提下找到彼此。

Cordis 正是为此设计的插件框架。它的核心思想是:一个程序就是挂载到同一个上下文中的一组插件;插件注册服务供其他插件使用,声明自己需要哪些服务,而它注册的一切都会在卸载时被自动撤销。加载顺序由依赖关系推导而来,重新加载一个插件就是先卸载再加载。DeepSeek Harness(下文简称 dsh)把这一思想毫无例外地应用于智能体运行框架:模型适配器、工具注册表、会话日志,乃至智能体循环本身,都是插件。

这两个真实项目的规模都很大。NanoCordis 保留它们的设计、舍弃它们的规模,让你能不受干扰地看清机制本身。

快速开始

要求:Node.js 20 或更高版本。

无需克隆即可试用——包已发布到 npm:

npx nano-cordis "Say hello"        # 或:npm install -g nano-cordis,然后运行 nano-cordis "Say hello"

若要阅读和修改代码,请克隆本仓库:

npm install
npm start -- "Say hello"

默认配置使用一个按脚本应答的模拟模型(无需 API 密钥)。它会请求执行一条 bash 命令,此时回答 y:

[tool] bash {"command":"echo hello from nano"}
Allow bash {"command":"echo hello from nano"}? [y/N] y
[result] hello from nano

[exit code: 0]

assistant> The command ran; that is all for this scripted reply.

不带参数运行 npm start 进入对话模式(提示符为 you>);用 npm start -- --resume  恢复已保存的会话(对话开始时会打印 id;日志保存在 .nano/sessions/)。

安装命令。npm install -g nano-cordis 会安装全局命令 nano-cordis;在克隆的仓库中执行 npm link,则把同名命令指向你的工作副本。两者使用的都是 npm start 的启动器,可在任意目录运行:若当前目录存在 cordis.yml 则读取该文件,否则使用随包附带的配置;会话日志写入运行目录下的 .nano/sessions/。

接入真实模型。在 cordis.yml 中,把 llm 条目替换为任意与 OpenAI 兼容的接口地址。密钥按指定的环境变量名读取,绝不写入文件:

- id: llm
name: ./src/harness/llm-openai.ts
config: { baseUrl: https://api.deepseek.com/v1, apiKeyEnv: DEEPSEEK_API_KEY, model: deepseek-v4-flash }

观察热更新。运行 npm start,然后编辑 cordis.yml:把审批条目改为 tools: [] 并保存。终端打印 [nano-cordis] applied cordis.yml,下一次 bash 调用不再询问。再编辑 src/harness/tool-bash.ts(例如它的 description)并保存:[nano-cordis] reloaded ./src/harness/tool-bash.ts——旧插件的注册全部消失,新插件的注册取而代之。若修改的是其他插件所依赖的条目(例如模型),这些插件也会先停止再重新启动;命令行会以一个新会话重新向你问候,这正是依赖机制在严格按其规则运作。

运行全部检查:npm test(95 个测试)与 npm run typecheck。

仓库地图与阅读顺序

请按以下顺序阅读;每个文件的头部注释都写明了它对应真实项目的哪一部分,以及阅读时应当留意的要点。

| # | 文件 | 说明 |
|---|---|---|
| 1 | bin.ts | 启动器:创建上下文,挂载加载器,挂载 cordis.yml。 |
| 2 | cordis.yml | 应用本身:每个条目对应一个插件。 |
| 3 | src/cordis/context.ts | 上下文:服务容器、plugin()、provide()、effect(),以及服务的调用方绑定视图。 |
| 4 | src/cordis/plugin.ts | 插件的三种形态与配置校验。 |
| 5 | src/cordis/fiber.ts | 一个正在运行的插件:状态、effect、启动与停止。 |
| 6 | src/cordis/refresh.ts | 调度器:随服务的出现与消失,启动或停止相应插件。 |
| 7 | src/cordis/events.ts | 事件总线与四种分发方式。 |
| 8 | src/cordis/service.ts | 提供服务的插件的基类。 |
| 9 | src/cordis/loader.ts | 读取 cordis.yml;按条目 id 挂载、重新挂载、卸载。 |
| 10 | src/cordis/hmr.ts | 监视文件,重新加载发生变化的部分。 |
| 11 | src/harness/session.ts(外加 freeze.ts) | 会话日志及由它推导出的消息;freeze.ts 是用来冻结已记录内容的小型辅助函数。 |
| 12 | src/harness/prompt.ts | 系统提示词的片段。 |
| 13 | src/harness/llm.ts、llm-openai.ts、llm-fake.ts | 模型服务定义与两个提供方。 |
| 14 | src/harness/tools.ts | 工具注册表与流水线。 |
| 15 | src/harness/agent-loop.ts | 一个轮次:询问模型、执行工具、如此反复。 |
| 16 | src/harness/shell.ts、shell-local.ts、tool-bash.ts | 一项能力的三个部分。 |
| 17 | src/harness/approval.ts | 一个策略插件。 |
| 18 | src/harness/persistence.ts | 保存与恢复会话。 |
| 19 | src/harness/cli.ts | 命令行界面——与其他插件一样,只是一个订阅者。 |

测试位于 tests/cordis/ 与 tests/harness/;tests/fixtures/agent.cordis.yml 在测试中仅凭一个文件启动整个智能体。

第一部分:最小化的 Cordis,逐条讲解思想

1. 插件是一个函数、一个类,或一个带 apply 的对象

// 函数形态:在加载器看来,具名导出的模块就是这种形态
export const name = 'hello'
export const inject = ['prompt']            // 这个插件需要的服务
export function apply(ctx: Context) {       // 所需服务齐备后才运行
ctx.prompt.section('hello', 10, 'Always greet the user first.')
}

类形态通常是 Service 的子类(见第 6 条),但任何类都可以挂载。加载器交给框架的是模块命名空间,即对象形态;若存在 default 导出,则以它取代整个命名空间。plugin.ts 判定形态并校验配置;context.ts 用 ctx.plugin(plugin, config) 完成挂载。

2. 上下文是服务容器;inject 声明插件所需的服务

ctx.provide('prompt', value) 把服务放入共享的服务表。插件在 inject 中列出所需的服务:全部存在时才启动,其中任何一个消失就停止,恢复后再重新启动(refresh.ts)。无须手工排列插件顺序——以任意顺序挂载,插件会依照依赖关系自行排序。在未声明 prompt 的插件中读取 ctx.prompt 会抛出错误(插件始终可以读取自己提供的服务,以及外层插件声明或提供的服务);读取一个已声明但已消失的服务同样抛出错误,而不是返回 undefined。ctx.get('prompt') 是读取可选服务的途径:当提供方正在运行时返回该服务,否则返回 undefined。

3. 插件在微任务中启动:await ctx.plugin(...)

ctx.plugin(x) 立即返回该插件的插件实例(fiber),并在当前代码执行完毕后再启动它。在使用 x 所提供的内容之前,应写 await ctx.plugin(x);真实框架也遵循同一规则。若插件配置无效或插件主体抛出错误,await 会重新抛出该错误,插件实例进入 failed 状态。插件实例的状态一次只推进一步:插件主体尚在运行时到达的停止请求,会等它结束后再执行(Cordis 以同样的方式串行化,并把进行中的那次变化称作 inertia)。

4. 你注册的一切都是 effect

ctx.effect(() => {
const timer = setInterval(tick, 1000)
return () => clearInterval(timer)         // 插件卸载时运行
})

ctx.on(...)、ctx.provide(...)、ctx.plugin(...) 都建立在 ctx.effect 之上;插件主体自身返回的函数也算作它的清理函数。卸载插件时按相反顺序运行这些清理函数——因此热更新不需要任何特殊支持:先卸载,再加载。ctx.effect 返回一个撤销函数:第一次调用运行清理并在完成时结束;之后的调用立即返回、不再等待(因此清理函数不会等待自身),而卸载始终会等待已在进行的那次运行结束。

5. 事件:四种分发方式

| 调用 | 行为 |
|---|---|
| ctx.emit(name, ...args) | 每个监听器按顺序运行;返回值一律忽略。 |
| await ctx.parallel(name, ...args) | 监听器同时运行;等待全部完成,所有失败合并为一个 AggregateError 统一报告。 |
| await ctx.serial(name, ...args) | 监听器逐个运行;第一个有意义的返回值胜出。 |
| ctx.waterfall(name, ...args, next) | 中间件式:每个监听器获得一个 next();不调用它就会短路其后的一切。 |

链式事件(waterfall)是拦截能力的来源——策略插件只要不调用 next(),就能否决一次工具调用——它附带一条规则:仅作观察的监听器必须调用 next()。事件名与签名通过扩展 Events 接口声明(declare module '../cordis/index.ts' { interface Events { ... } });ctx. 的类型以同样的方式声明(interface Context { prompt: Prompt })。仅为读取这些类型声明而写的空导入是 import type {} from './tools.ts'——在 src/harness/ 下多个文件的开头都能看到这一行。

6. 服务与调用方绑定的视图

Service 的子类用 super(ctx, name) 注册自身。当另一个插件通过 ctx. 读取它时,得到的是同一对象的一个视图,视图中的 this.ctx 属于调用方。因此当 tools.register(...) 内部执行 this.ctx.effect(...) 时,这次注册归调用 register 的插件所有,并随该插件卸载而消失——尽管代码写在 tools 服务里。在方法内部读取服务(this.ctx.llm)仍按服务自身的 inject 检查。Cordis 把这一机制称为 traceable;context.ts 中的 bindToCaller 就是它的实现。

7. 配置在插件启动前校验

插件导出一个 Config 校验器(任何符合 Standard Schema 的校验器均可;本仓库使用 schemastery)。校验会填充默认值,并把校验后的对象传给 apply;cordis.yml 中的错误配置会在报错中指出字段名(含路径,例如 inner.deep),插件绝不会在配置残缺的情况下启动。

8. cordis.yml 与热更新

加载器读取一组条目 { id, name, config, disabled },把每一条挂载为自己的子插件;再次读取时按 id 比对:被删除的条目卸载,发生变化的重新挂载,新增的挂载。模块加载失败的条目会被报告并记录,下一次保存配置文件或该模块时会重试;若保存后的模块连加载都无法完成,这次保存不会改变任何东西。hmr.ts 监视目录并调用加载器。二者本身也都是插件。

编写插件的五条规则

1. 凡用 ctx. 读取的服务,都必须写入 inject;只有可选服务才使用 ctx.get(name)。
2. 一切注册都要经过 ctx.effect(或建立在它之上的 ctx.on、ctx.provide、ctx.plugin)——不要保留框架不知晓的监听器或定时器。
3. 在使用 x 所提供的内容之前,先 await ctx.plugin(x)。
4. 服务方法中的 this.ctx 属于调用方。属于服务自身的注册,要使用构造时保存的上下文(参见 Loader 保存 host 的方式)。
5. 不要在服务中使用 #private 字段;调用方绑定的视图无法读取它们。

第二部分:最小智能体,逐条讲解机制

1. 会话日志是唯一数据源

session.ts 维护一份只追加的事件列表:turn/start、step/start、user/message、request/header、assistant/message、tool/call、tool/result、step/end、turn/end。append() 是唯一的写入入口(session.events 返回冻结的副本);每条事件获得下一个 seq,随即被冻结,并作为 session/event 广播——广播进行期间监听器不得再追加事件,因此各方看到的事件顺序都与日志一致。发给模型的消息由 deriveMessages() 从日志推导而来,从不单独保存——因此回放、恢复与检查读取的都是同一份数据。

2. 未经记录的内容不会到达模型

每次请求之前,只要请求头(模型、系统提示词、工具定义)发生了变化,循环就先记录一条 request/header,再用“最新记录的请求头 + 推导出的消息”构造请求。插件可以通过 agent/request 链式事件调整请求头——但只能调整会被记录的部分。在 dsh 中,“模型可见即已记录”是一条始终生效的规则,并在运行时校验;在这里,它由构造方式直接保证。

3. 轮次与步骤

一个轮次从用户消息开始,在模型不再调用工具时结束(completed),也会因达到步骤上限(max-steps)或发生错误而结束。一个步骤是一次模型请求,加上它产生的工具调用。agent-loop.ts 在 finally 中写入 turn/end,因此日志必定有始有终。

4. 工具:注册表与流水线

tools.ts 保存工具定义,并让每一次调用都经过同一条流水线:tools/pre-execute(放行或拒绝)→ 工具主体 → tools/post-execute(改写结果)。任何失败——未知的工具、被拒绝的调用、无效的参数、抛出错误的工具主体——都会变成一个带 isError: true 的结果;循环完全不需要捕获异常。schemas()(模型看到的)与 execute()(可以执行的)读取同一张表,因此被移除的工具会同时从两处消失。

5. 一项能力,三个部分

shell 能力按照 dsh 拆分每一项可替换能力的方式拆分:shell.ts 是服务定义(请求、规格、结果,以及显式填充默认值的 resolve()),shell-local.ts 是提供方(本机的 bash),tool-bash.ts 是把它交给模型使用的使用方。使用方只依赖服务定义;在 cordis.yml 中替换提供方所在的一行,其余一切不变。模型服务以同样的方式拆分(llm.ts / llm-openai.ts / llm-fake.ts)。

6. 策略是插件

approval.ts 监听 tools/pre-execute;对由它守护的工具,它会发出 approval/request,除非有应答方明确回答 true,否则一律拒绝(回答 "yes" 或 1 都算拒绝,因此有缺陷的应答方只会拒绝,绝不会放行)。命令行通过向你提问来作答;测试自动作答;无人应答即拒绝。从 cordis.yml 中删除该条目,审批便不复存在——循环与工具从头到尾都未曾参与。

7. 提示词片段

prompt.ts 用按序号排序的具名片段组装系统提示词。每个片段都是一个 effect,归添加它的插件所有。

8. 持久化与恢复

persistence.ts 把每条事件追加到 .nano/sessions/.jsonl;恢复会话,就是以文件中的事件为历史重新创建会话(这正是 Session 接受一段不予广播的初始历史的原因)。停在轮次中间的日志会被拒绝,而不会靠猜测续写。

9. 命令行只是订阅者

cli.ts 打印 session/event,应答 approval/request,并把输入交给 agentLoop.run()。循环中没有任何部分知道它的存在;网页界面将只是做同样事情的另一个插件。

10. cordis.yml 就是应用

打开这个文件:会话、持久化、提示词、模型、工具、shell、bash 工具、审批、循环、命令行、热更新——十一个条目,顺序任意。更换模型、移除审批、替换 shell:只需编辑该文件;若 hmr 正在运行,还能当即看到变更生效。

一个轮次,逐步展开

用户输入 → cli → agentLoop.run(session, text)
写入 turn/start
步骤 1..maxSteps:
写入 step/start                          (第一步还会写入 user/message)
config = waterfall('agent/request', { model: llm.model, system: prompt.render() })
header = config + tools.schemas()        → 有变化则写入 request/header
request = 最新记录的请求头 + session.deriveMessages()
reply = llm.chat(request)                → 写入 assistant/message
没有工具调用 → 写入 step/end、turn/end(completed)→ 结束
对每个调用:写入 tool/call → tools.execute → [pre-execute ▷(approval ▷)→ 工具主体 → post-execute ▷] → 写入 tool/result
写入 step/end

动手练习

1. 添加一个工具。把 tool-bash.ts 复制为返回当前日期的 tool-date.ts,在 cordis.yml 中增加一个条目,然后观察它出现在下一条 request/header 中。
2. 更换模型。把 llm 条目指向 llm-openai.ts 与你的接口地址。其余一切不变。
3. 编写一个策略。写一个监听 tools/pre-execute 的插件,拒绝任何包含 rm -rf 的 bash 命令。对其余情况务必调用 next()。
4. 实时修改角色设定。在 npm start 运行期间,编辑 cordis.yml 中的 persona: 并保存。提示词插件重新挂载,循环与命令行随之重启,命令行会以一个新会话向你问候;发送一条消息,然后在新会话的 .nano/sessions/.jsonl 中查看新的 request/header。
5. 只读的 shell。编写继承 LocalShell 的 shell-readonly.ts,让它的 run() 在调用 super.run() 之前拒绝任何不以 ls、cat、echo 开头的命令,再用它替换 shell-local.ts。

有意省略的部分

未实现的 Cordis 功能(整块缺失的功能列在此处;凡省略之处对应代码中的具体位置,那里另有一条 // Omitted: 注释):隔离的服务域与按服务的配置拦截;按上下文过滤监听器(dsh 按智能体划分作用域的基础);bail 与 once;生成器形式的 effect 与可等待的异步启动;可调用的服务、Service.check、异步初始化;ctx.set / accessor / mixin;对象形式 inject 所携带的按服务配置(其键名仍被视为所需服务);失败插件的自动重试;带 internal/update 的原地配置更新;内部事件;日志服务;加载器的分组、事务回滚、写回文件、!!js 表达式;基于 Node 模块关系图的热更新。

NanoCordis 有意采用不同行为之处:只在插件处于运行状态时接受 effect;清理函数逐个运行而非并发运行;emit 会隔离抛出错误的监听器;失败的插件保持失败状态;链式事件的 next 按监听器逐个组合;类插件按 class 关键字判定;缺少 id 的条目会被拒绝;配置变化按重新挂载处理;模块重新加载失败时按条目逐个报告,不做任何回滚。

未实现的 dsh 机制:流式输出与逐片段记录;带 follow-up、steer、inject 三种输入的收件箱;agent/pre-step、agent/turn-stopping、agent/request-error;取消;智能体注册表与句柄;作用域与按会话的预设;子智能体、工作流、后台任务;上下文压缩;沙箱;多提供方路由与重试;审批的审计事件与策略;tools/execute 包装、guard、tools/result;并行工具调用;崩溃修复;分叉;投影、检索、遥测;bundle、profile 与补丁层;网页界面、SDK 与 ACP。日志直接使用 OpenAI 的消息格式,而不是 dsh 那种不依赖特定提供方的格式。

真实代码的对应位置:

| NanoCordis | 真实项目 |
|---|---|
| src/cordis/context.ts | cordis packages/core/src/context.ts、reflect.ts |
| src/cordis/plugin.ts | cordis registry.ts |
| src/cordis/fiber.ts、refresh.ts | cordis fiber.ts |
| src/cordis/events.ts | cordis events.ts |
| src/cordis/service.ts | cordis service.ts |
| src/cordis/loader.ts | @cordisjs/plugin-loader、plugin-include |
| src/cordis/hmr.ts | @cordisjs/plugin-hmr |
| src/harness/session.ts | dsh packages/core/session |
| src/harness/persistence.ts | dsh packages/session/session-persistence、-jsonl |
| src/harness/prompt.ts | dsh packages/core/system-prompt |
| src/harness/llm.ts | dsh packages/llm/llm、llm-deepseek |
| src/harness/tools.ts | dsh packages/core/tools |
| src/harness/shell.ts、tool-bash.ts | dsh packages/shell/shell、bash-local、tool-bash |
| src/harness/approval.ts | dsh packages/interaction/user-approval 与 tools/pre-execute 策略写法 |
| src/harness/agent-loop.ts | dsh packages/core/agent、agent-loop |
| src/harness/cli.ts | dsh packages/bundle/headless、apps/cli |

测试

npm test 运行 vitest:tests/cordis/ 覆盖 effect、依赖、启动时序、事件、调用方绑定、配置、加载器与热更新;tests/harness/ 覆盖会话日志、工具流水线、完整轮次、审批、持久化、按脚本应答的模型,以及从 tests/fixtures/agent.cordis.yml 启动整个智能体。

致谢与许可

Cordis 由 cordiverse 组织开发,其设计见论文 A Programming Paradigm for Spatiotemporal Composability。DeepSeek Harness 由 DeepSeek 开发。NanoCordis 出于教学目的重新实现了它们的思想,与两个项目均无从属关系。采用 MIT 许可证。

标识由十一个方块组成,对应 cordis.yml 的十一个条目;红色的一块是正在被重新加载的插件。

上游仓库有新提交时邮件通知你(每天最多一封,无更新不打扰),随时一键退订。

💬 加入 DPharness 群聊

插件用法、部署报错、新插件第一时间同步——群里问,比一个人翻文档快。

点击加入 QQ 群
DPharness 群聊二维码,手机 QQ 扫码进群
扫码进群