docs: translate remaining non-README documentation

This commit is contained in:
Tianyi Cui
2026-07-26 02:33:29 +08:00
parent 5f04c92f15
commit 30db52fa4b
102 changed files with 3859 additions and 82 deletions

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
01-first-plugin.md: b730b7ad7dc9ebd2e5dc8af4f7a83bd58ac7d4d8
01-first-plugin.zh.md: 1e6901c048f5268ddead0faca85eaf5eef9c7533

View File

@@ -1,5 +1,7 @@
# 1. Your first plugin
English | [中文](01-first-plugin.zh.md)
In the loader configuration used here, a Cordis plugin module named-exports an `apply` function. When Cordis loads it, it calls `apply` with a **context** — the `ctx` object through which the plugin registers everything it contributes.
## Write the plugin

View File

@@ -0,0 +1,95 @@
# 1. 编写第一个插件
[English](01-first-plugin.md) | 中文
在本教程使用的 loader 配置中Cordis 插件模块通过命名导出提供 `apply` 函数。Cordis 加载模块时,会用一个 **上下文** 调用 `apply`;该上下文就是 `ctx` 对象,插件通过它注册自己贡献的所有内容。
## 编写插件
`tmp/cordis-tutorial` 目录中(参见[环境设置](index.md#setup))创建 `hello.ts`
```ts
import type { Context } from 'cordis'
export const name = 'hello'
export function apply(ctx: Context) {
console.log('hello from my first plugin')
}
```
`name` 导出项是可选的显示元数据;它用于在诊断信息中标识插件。
## 组合应用
本教程的启动器通过配置组装应用。创建 `cordis.yml`
```yaml
- name: './hello.ts'
```
该文件是一组 Cordis 配置项的列表。`name` 是模块指定符,可以是相对路径或 NPM 包packageloader 会挂载每个配置项。各项会并发启动,因此它们在列表中的位置不保证插件的加载先后;顺序由服务依赖(`inject`,参见[第 3 章](03-services.md))决定,而非文件中的位置。
## 运行
```sh
node --import tsx ../../vendor/cordis/bin.js
```
预期输出:
```
hello from my first plugin
```
当没有任何内容继续运行时,进程会自行退出。具体过程如下:
1. 启动器创建根 `Context`,并挂载 **Loader** 插件。
2. Loader 读取 `cordis.yml`,解析 `./hello.ts`,然后将其作为子插件挂载。
3. Cordis 调用你的 `apply(ctx)`
你的文件中没有框架启动代码:插件描述自己的贡献,`cordis.yml` 则组合应用。例如,[TUI agent智能体](../../examples/tui-agent/cordis.yml) 就是一个更长的插件组合。
## 其他两种插件形态
函数是最常见的形态,但 Cordis 接受三种形态:
```ts
import { Service, type Context } from 'cordis'
// 1. Function plugin (what you just wrote).
export function apply(ctx: Context) {}
// 2. Object plugin: an object with an `apply` method.
export const objectPlugin = {
name: 'object-plugin',
apply(ctx: Context) {},
}
// 3. Class plugin: a Service subclass (covered in chapter 3).
export class MyService extends Service {
constructor(ctx: Context) {
super(ctx, 'myTutorialService')
}
}
```
在你需要公开服务之前,请一直使用函数形态;[第 3 章](03-services.md)介绍了何时应当使用类形态。
## 尝试制造错误
`apply` 抛出异常:
```ts ignore-check
export function apply(ctx: Context) {
throw new Error('apply exploded')
}
```
再次运行:进程会因该错误而终止。插件加载失败必须明确报错,不会仅跳过该配置项。
还需要尽早了解一个例外:如果某个配置项的模块无法被 **解析**例如路径或包名拼写错误Cordis 会通过 logger 服务报告错误,而不会使进程崩溃。在启动阶段,这条报告可能在 console 导出器开始观察之前丢失。如果新增配置项似乎没有任何效果,请先检查拼写。
下一章:[生命周期与 effect](02-lifecycle-and-effects.md):插件卸载时会发生什么。
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-harness/deepseek-harness)

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
02-lifecycle-and-effects.md: f1b39e06e9d25c51ab2d76503025e2b6ffe90c73
02-lifecycle-and-effects.zh.md: a6021ed7475a0045d480810747244274eb5b4198

View File

@@ -1,5 +1,7 @@
# 2. Lifecycle and effects
English | [中文](02-lifecycle-and-effects.zh.md)
A Cordis plugin can be unloaded by a config edit, hot reload, explicit disposal, or loss of a required service. Registrations made through Cordis APIs are effects and are undone when their owning plugin unloads; resources managed outside those APIs must be wrapped in `ctx.effect()`.
## Effects

View File

@@ -0,0 +1,98 @@
# 2. 生命周期与 effect
[English](02-lifecycle-and-effects.md) | 中文
Cordis 插件可能因配置编辑、热重载、显式资源释放或所需服务消失而卸载。通过 Cordis API 建立的注册属于 effect会在所属插件卸载时撤销在这些 API 之外管理的资源必须包装在 `ctx.effect()` 中。
## Effect
对于 Cordis 尚未管理的资源,例如定时器、连接或 watcher应将其包装在 `ctx.effect()` 中并返回 disposerdispose资源释放函数
创建 `lifecycle.ts`,将它放在 `tmp/cordis-tutorial` 中:
```ts
import type { Context } from 'cordis'
export const name = 'lifecycle-demo'
function heartbeat(ctx: Context) {
console.log('heartbeat plugin loading')
ctx.effect(() => {
const timer = setInterval(() => console.log('tick'), 200)
return () => {
clearInterval(timer)
console.log('heartbeat cleaned up')
}
})
}
export function apply(ctx: Context) {
// Mount a child plugin and keep its fiber to dispose it later.
const fiber = ctx.plugin(heartbeat)
// The demo timer is itself an effect: if THIS plugin is unloaded first,
// the pending callback is cancelled instead of firing on a dead app.
ctx.effect(() => {
const timer = setTimeout(async () => {
await fiber.dispose()
console.log('disposed')
process.exit(0)
}, 700)
return () => clearTimeout(timer)
})
}
```
`cordis.yml` 指向该文件:
```yaml
- name: './lifecycle.ts'
```
运行(`node --import tsx ../../vendor/cordis/bin.js`)后会得到:
```
heartbeat plugin loading
tick
tick
tick
heartbeat cleaned up
disposed
```
请留意三点:
- `ctx.plugin(heartbeat)` 会把一个**来自代码**的函数挂载为插件,这与 YAML loader 为每个配置项执行的操作相同。函数插件不需要 `apply` 方法Cordis 会直接调用该函数,其名称只用于诊断。只有对象形态才要求 `apply` 方法,例如 `ctx.plugin({ apply(ctx) { /* ... */ } })`。调用会返回一个 **fiber**,即一个已加载插件实例的运行时句柄。
- effect 主体在加载期间运行;它返回的 disposer 在卸载期间运行。对于生命周期与插件一致的资源,你绝不需要自行调用 disposer。
- `fiber.dispose()` 会等该插件的所有清理工作(包括异步 disposer完成后才结束并递归卸载它挂载的所有子插件。
## Fiber 状态机
每个已加载插件实例都拥有一个 fiber并依次经过以下状态
```
PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
↘ FAILED
```
- **PENDING**:已经声明,但所需服务(第 3 章)尚不可用。
- **LOADING / ACTIVE**`apply` 正在运行/已经完成。
- **FAILED**`apply` 或配置校验抛出异常。
- **UNLOADING / DISPOSED**disposer 正在运行/一切均已拆除。
你会在[第 6 章](06-composition-and-hmr.md)再次遇到 PENDING它通常就是「为什么我的插件没有输出」的答案。
## 已经属于 effect 的操作
你很少需要亲自编写 `ctx.effect()`,因为内置注册 API 本身已经是 effect
- `ctx.on(event, listener)`:监听器会在卸载时移除([第 4 章](04-events.md))。
- `ctx.plugin(child)`:子插件会随父插件一同 dispose。
- 服务注册属于 effect。`ctx.tools.register(...)` 等 harness 注册表也会把返回的 disposer 附着到调用插件上,因此会自动回卷([第 7 章](07-into-the-harness.md))。
对于 Cordis 不管理的资源,应在 `ctx.effect()` 内获取它,并返回用于释放资源的 disposer。此后 Cordis 会在卸载期间调用该释放逻辑,热重载时也不例外。
有一项顺序注意事项disposer 会按注册顺序的逆序启动,但多个**异步** disposer 会并发运行。如果拆除步骤必须按顺序执行,请把它们放在同一个 disposer 中,并在其中依次等待每步完成。
下一章:[服务](03-services.md):插件如何共享功能。
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-harness/deepseek-harness)

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
03-services.md: 5848132c6ad18338fa893954d45fc20005db6199
03-services.zh.md: 3c77d0451df9062f1a344e7474e6be141b709197

View File

@@ -1,5 +1,7 @@
# 3. Services
English | [中文](03-services.zh.md)
A **service** is a named capability one plugin provides and other plugins consume through `ctx`. In the harness, `ctx.tools`, `ctx.llm`, and `ctx.agents` are services. A consumer names the capability, such as `'tools'`, rather than importing its provider, so configuration can select a provider without changing the consumer.
## Provide a service

View File

@@ -0,0 +1,98 @@
# 3. 服务
[English](03-services.md) | 中文
**服务**是一个插件提供、其他插件通过 `ctx` 消费的命名功能。在 harness 中,`ctx.tools``ctx.llm``ctx.agents` 都是服务。消费方只命名 `'tools'` 之类的功能,而不导入其提供方,因此配置可以选择提供方,无需修改消费方。
## 提供服务
创建 `greeter.ts`,将它放在 `tmp/cordis-tutorial` 中:
```ts
import { Service, type Context } from 'cordis'
declare module 'cordis' {
interface Context {
greeter: GreeterService
}
}
export class GreeterService extends Service {
constructor(ctx: Context) {
super(ctx, 'greeter')
}
greet(who: string) {
return `Hello, ${who}!`
}
}
export const name = 'greeter'
export function apply(ctx: Context) {
ctx.plugin(GreeterService)
}
```
两部分协同工作:
- **运行时**`super(ctx, 'greeter')` 以名称 `greeter` 注册该实例。此后,任何插件都可以通过 `ctx.greeter` 访问它。注册属于 effect卸载提供方时会移除该服务。
- **编译时**`declare module 'cordis'` 块使用 TypeScript 声明合并,把 `greeter` 加入 `Context` 接口,使 `ctx.greeter` 在各处都能通过类型检查。它不会生成代码;没有该声明时,服务在运行时仍能工作,但消费方会失去类型安全。
`Service` 子类本身就是插件(第 1 章介绍的类形态),因此 `ctx.plugin(GreeterService)` 会像挂载其他插件一样挂载它。
## 使用 `inject` 消费服务
创建 `consumer.ts`
```ts
import type { Context } from 'cordis'
export const name = 'consumer'
export const inject = ['greeter']
export function apply(ctx: Context) {
console.log(ctx.greeter.greet('world'))
}
```
`inject` 列出该插件需要的服务。Cordis 会让插件保持 PENDING直到列出的每项服务都存在因此在 `apply` 内可以保证 `ctx.greeter` 已经就绪。`cordis.yml` 中的加载顺序无关紧要:决定插件何时启动的是依赖关系,而不是文件顺序。
组合并运行:
```yaml
- name: './greeter.ts'
- name: './consumer.ts'
```
```
Hello, world!
```
交换 `cordis.yml` 中两行的顺序后重新运行,输出仍然相同。尝试彻底移除 `./greeter.ts`:消费方会保持 PENDING不输出任何内容既不崩溃也不会只运行一部分。处于 PENDING 的 fiber 也不会让 Node 的事件循环保持活跃,因此如果组合中没有其他运行项,进程会静默地以状态码 0 退出。[第 6 章](06-composition-and-hmr.md)介绍如何诊断这种状态。
## 加载后仍会跟踪依赖关系
`inject` 并非一次性的启动检查。如果应用运行期间所需服务消失,例如提供方被卸载或热替换,每个依赖插件也会随之卸载,并在服务恢复后再次加载。结合 effect[第 2 章](02-lifecycle-and-effects.md)),这能防止运行中的消费方保留对不可用服务的引用:依赖消失时,它自己的注册也会回卷。
这也是配置中可以替换服务的原因:卸载 `dsh-bash-local` 配置项,挂载另一个 `bash` 提供方,所有注入 `'bash'` 的插件都会干净地重启并使用新实现。
## 可选依赖
`inject` 用于硬性依赖。如果某项功能缺失时插件仍可运行,请跳过 `inject`,并在使用处探测:
```ts ignore-check
export function apply(ctx: Context) {
// undefined when no provider is loaded; the plugin still runs.
const greeter = ctx.get('greeter')
console.log(greeter?.greet('maybe') ?? 'no greeter available')
}
```
## 命名
每个应用中的服务名称共用一个扁平命名空间。请为自有服务添加有辨识度的前缀或命名空间harness 已占用 `tools` 和 `llm` 等普通名称);生成的[服务目录](../cordis-catalog/services.md)列出 harness 注册的每个名称。
下一章:[事件](04-events.md):无需共享服务即可通信。
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-harness/deepseek-harness)

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
04-events.md: 18f39dc1b693e5fb7e1793ec4b7dcac9cf24db95
04-events.zh.md: f55a61ff2f43ea42968893d07eb92ea0613b921a

View File

@@ -1,5 +1,7 @@
# 4. Events
English | [中文](04-events.zh.md)
Services support direct calls; **events** let a plugin announce something without knowing which plugins listen. The harness uses events for interactions such as tool results, model requests, and approval decisions.
## Declare, emit, listen

View File

@@ -0,0 +1,144 @@
# 4. 事件
[English](04-events.md) | 中文
服务支持直接调用;**事件**让插件无需知道有哪些插件正在监听就能发出通知。harness 使用事件处理工具结果、模型请求和审批决定等交互。
## 声明、发出与监听
创建 `stats.ts`,将它放在 `tmp/cordis-tutorial` 中。它是一项负责计数并在每次变化时发出通知的服务:
```ts
import { Service, type Context } from 'cordis'
declare module 'cordis' {
interface Context {
stats: StatsService
}
interface Events {
'stats/report'(name: string, count: number): void
}
}
export class StatsService extends Service {
private counts = new Map<string, number>()
constructor(ctx: Context) {
super(ctx, 'stats')
}
bump(name: string) {
const next = (this.counts.get(name) ?? 0) + 1
this.counts.set(name, next)
this.ctx.emit('stats/report', name, next)
}
}
export const name = 'stats'
export function apply(ctx: Context) {
ctx.plugin(StatsService)
}
```
`interface Events` 合并与第 3 章的 `interface Context` 合并在事件系统中相互对应:它声明事件名称及其监听器签名,因此 `ctx.emit``ctx.on` 都具有完整类型。`namespace/action` 命名约定让扁平的事件命名空间保持易读。
创建 `reporter.ts`
```ts ignore-check
import type { Context } from 'cordis'
import type {} from './stats.ts'
export const name = 'reporter'
export const inject = ['stats']
export function apply(ctx: Context) {
ctx.on('stats/report', (name, count) => {
console.log(`[stats] ${name} -> ${count}`)
})
ctx.stats.bump('tool_call')
ctx.stats.bump('tool_call')
ctx.stats.bump('prompt')
}
```
`import type {} from './stats.ts'` 行不会在运行时导入任何内容;它的作用是让 TypeScript 看到声明合并。组合并运行:
```yaml
- name: './stats.ts'
- name: './reporter.ts'
```
```
[stats] tool_call -> 1
[stats] tool_call -> 2
[stats] prompt -> 1
```
因为 `ctx.on()` 属于 effect监听器会随插件一同消失绝不需要手动维护 `removeListener`。
## 分发模式
`emit` 是 5 种分发模式之一。事件采用哪种模式是其契约的一部分,决定了监听器能否返回值、能否并发运行,以及能否彼此短路:
| 模式 | 调用 | 语义 |
|---|---|---|
| emit | `ctx.emit(name, ...args)` | 同步广播;不会等待或收集返回的 promise 与值。 |
| parallel | `await ctx.parallel(name, ...args)` | 所有监听器并发运行,并一同等待。 |
| serial | `await ctx.serial(name, ...args)` | 监听器按顺序运行并等待;第一个非 `null`/`false`/`undefined` 返回值胜出,并停止后续监听器。 |
| bail | `ctx.bail(name, ...args)` | serial 的同步版本。 |
| waterfall瀑布式事件 | `ctx.waterfall(name, ...args, next)` | 环绕中间件,见下文。 |
每个 harness 事件都会在生成的[事件目录](../cordis-catalog/events.md)中记录其模式。
## Waterfall转换或短路
waterfall 是实现拦截的模式。每个监听器都会收到参数和一个 `next()` continuation它可以转换 `next()` 的返回值,也可以不调用 `next()` 就直接返回从而短路链条的其余部分。Cordis 文档把后一种行为称为否决。创建 `waterfall-demo.ts`
```ts
import type { Context } from 'cordis'
declare module 'cordis' {
interface Events {
'demo/transform'(input: string, next: () => Promise<string>): Promise<string>
}
}
export const name = 'waterfall-demo'
export function apply(ctx: Context) {
// Listener 1: wrap the downstream result.
ctx.on('demo/transform', async (input, next) => {
const downstream = await next()
return downstream.toUpperCase()
})
// Listener 2: short-circuit when it owns the decision.
ctx.on('demo/transform', async (input, next) => {
if (input.includes('blocked')) return '** blocked **'
return next()
})
void (async () => {
console.log(await ctx.waterfall('demo/transform', 'hello', async () => 'hello'))
console.log(await ctx.waterfall('demo/transform', 'blocked words', async () => 'blocked words'))
})()
}
```
让 `cordis.yml` 只指向该文件并运行:
```
HELLO
** BLOCKED **
```
按顺序看第二行如何产生:监听器 1 先运行并调用 `next()`,从而调用监听器 2监听器 2 看到 `blocked` 后直接返回而不调用 `next()`,因此最内层默认逻辑(传给 `ctx.waterfall` 的函数)从未运行;返回途中,监听器 1 再把替换消息转换为大写。
由此得到一项纪律:**只负责观察或标注的 waterfall 监听器必须调用 `next()`**;不调用就直接返回代表有意短路。如果日志监听器忘记调用 `next()`,会悄无声息地吞掉所有下游的默认行为。这一点极其重要,已成为本仓库的常设规则([waterfall 语义](../cordis-primer.md#cordis-waterfall-semantics))。
harness 使用 waterfall 处理协作插件可以包装或回答的决策:[`agent/request`](../cordis-catalog/events.md#agentrequest--waterfall) 允许插件替换模型调用配置,[`approval/request`](../cordis-catalog/events.md#approvalrequest--waterfall) 允许策略代替用户作答。
下一章:[配置](05-config.md):来自 `cordis.yml` 的插件选项。
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-harness/deepseek-harness)

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
05-config.md: fc19add239636fa9e7071d9c77e48595caec1f08
05-config.zh.md: 52a75e40672c9a08d285677dd14dcd404b925e5a

View File

@@ -1,5 +1,7 @@
# 5. Configuration
English | [中文](05-config.zh.md)
Each `cordis.yml` entry can carry a `config` block, and the plugin declares a schema that validates it before `apply` runs. Bad config fails the load with a precise error — the plugin never starts half-configured.
## A configurable plugin

View File

@@ -0,0 +1,84 @@
# 5. 配置
[English](05-config.md) | 中文
每个 `cordis.yml` 配置项都可以携带 `config` 块,插件则声明一个 schema在运行 `apply` 前验证该块。错误配置会导致加载失败,并给出准确的错误:插件绝不会在配置不完整时启动。
## 可配置插件
创建 `config-demo.ts`,并将其放在 `tmp/cordis-tutorial` 中:
```ts
import type { Context } from 'cordis'
import Schema from 'schemastery'
export const name = 'config-demo'
export interface Config {
greeting: string
targets: string[]
}
export const Config: Schema<Config> = Schema.object({
greeting: Schema.string().default('Hello'),
targets: Schema.array(String).default(['world']),
})
export function apply(ctx: Context, config: Config) {
for (const target of config.targets) {
console.log(`${config.greeting}, ${target}!`)
}
}
```
导出的 `Config` 既是 TypeScript 接口,也是同名的运行时 schema消费方获得类型Cordis 获得验证器。本仓库使用 [Schemastery](https://github.com/shigma/schemastery) 定义 schemaCordis 本身接受任意 [Standard Schema](https://standardschema.dev/) 验证器,因此将普通对象导出为 `Config` 无法工作。
对其进行配置:
```yaml
- name: './config-demo.ts'
config:
targets: ['alpha', 'beta']
```
运行:
```
Hello, alpha!
Hello, beta!
```
未提供 `greeting`,因此 schema 默认值会将其补齐:`apply` 始终会收到完整且经过验证的配置。
## 明确报错
现在向它传入无效内容:
```yaml
- name: './config-demo.ts'
config:
targets: 'not-an-array'
```
```
ValidationError: invalid config:
- $.targets expected array but got not-an-array (at targets)
```
插件的 fiber 进入 FAILED 状态,本教程的启动器打印错误后以状态码 1 退出。如果某个插件的 schema 有效配置命名了不可用的资源或提供方,该插件也应当在能解析该引用时立即拒绝。
## 计算得到的配置值
本仓库使用的 loader 支持 `!!js` 标签,用于必须在加载时计算的配置值,例如从环境中读取 API key
```yaml
- name: '@deepseek-ai/dsh-llm-deepseek'
config:
apiKey: !!js process.env.DEEPSEEK_API_KEY
```
`!!js` **仅在 `config` 内有效**。配置项元数据(`name``id``disabled``inject` 等)是静态的;`disabled: !!js ...` 会生成一个真值表达式对象,始终禁用该配置项。详见 [loader 配置](../cordis-primer.md#loader-configuration)。
下一章:[组合与 HMR](06-composition-and-hmr.md):将 `cordis.yml` 视为应用。
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-harness/deepseek-harness)

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
06-composition-and-hmr.md: 66d6a9d93fe39baa881940ba32388979e2678505
06-composition-and-hmr.zh.md: ebe63fc26607ae6d9344c4795a7975496ed901b5

View File

@@ -1,5 +1,7 @@
# 6. Composition and HMR
English | [中文](06-composition-and-hmr.zh.md)
Every capability built so far is a plugin, and `cordis.yml` selects the application's plugin tree. This chapter changes that composition, hot-reloads a plugin, and diagnoses a plugin that never loads.
## Entries are more than a name

View File

@@ -0,0 +1,113 @@
# 6. 组合与 HMR热模块替换
[English](06-composition-and-hmr.md) | 中文
到目前为止构建的每项功能都是插件,`cordis.yml` 则选择应用的插件树。本章会改变这种组合、热重载一个插件,并诊断始终无法加载的插件。
## 配置项不只有名称
配置项除了 `name``config`,还接受其他元数据:
```yaml
- id: greeter # stable identity for this entry
name: './greeter.ts'
- id: consumer
name: './consumer.ts'
disabled: true # keep the entry, skip mounting it
```
`id` 为配置项提供稳定标识,使 loader 能区分修改现有配置项与先删除再添加。`disabled: true` 会卸载插件而不删除其配置项;改回原值后,插件以及所有因依赖其服务而处于 PENDING 的插件都会再次加载。
组可以嵌套一份配置项子列表,并将其作为一个单元加载和卸载;`isolate` 则为一个组提供某项服务名称的独立实例,因此两个组可以各自看到配置不同的 `bash`,互不影响。这些概念值得在用到之前先了解;[Cordis 入门](../cordis-primer.md)和[服务隔离示例](../user/develop/framework/service.md#service-isolation)介绍了详细内容。
## 热模块替换
卸载会释放 effect[第 2 章](02-lifecycle-and-effects.md)),加载则遵循依赖关系([第 3 章](03-services.md)),因此 HMR 可以先卸载、再加载,以替换正在运行的插件。`@cordisjs/plugin-hmr` 插件会监视文件,并在保存时执行这一过程。
`tmp/cordis-tutorial` 中编写 `cordis.yml`
```yaml
- id: logger
name: '@cordisjs/plugin-logger-console'
- id: timer
name: '@cordisjs/plugin-timer'
- id: hmr
name: '@cordisjs/plugin-hmr'
config:
root: ['.']
- id: hello
name: './hello.ts'
```
列表中增加了两个支持插件HMR 通过 Cordis logger 服务记录日志,因此没有 console exporter 时看不到其消息;它还会 `inject` `timer` 服务来实现去抖,如果没有 `@cordisjs/plugin-timer`,它就会永远停在 PENDING而且不发出任何提示。下一节就讨论这种静默状态。
HMR 通过 Loader 的原生辅助工具读取 Node 的 loader 内部结构。请在 tsx 下运行 Cordis
```sh
node --import tsx ../../vendor/cordis/bin.js
```
现在编辑 `hello.ts`,修改日志消息并保存:
```
hello from my first plugin
2026-07-22 15:44:36 [I] hmr watching [ '.' ]
2026-07-22 15:44:39 [I] hmr reload plugin at hello.ts
hello from my EDITED plugin
```
旧实例先卸载(其所有 effect 都会回卷),新代码随后加载,`apply` 再次运行。按 Ctrl-C 停止进程。编辑 `cordis.yml` 本身也会触发更新loader 按 `id` 比较配置项,只挂载、卸载或重新配置发生变化的部分。这就是上述配置项显式携带 `id` 的原因:不带该字段的配置项在每次读取时都会获得一个新生成的 id所以只要配置文件发生任何编辑即使自身文本未变它也会被视为先删除再添加并重新挂载。
## 诊断始终无法加载的插件
依赖驱动加载也有另一面:如果插件的 `inject` 指定了无人提供的服务,它就会一直等待,不输出任何内容。这不是错误,因为 PENDING 是合法状态,提供方可能稍后才挂载。
你可以直接查看这些状态。每个上下文都能枚举插件注册表;创建 `diagnose.ts`
```ts
import { FiberState, type Context } from 'cordis'
export const name = 'diagnose'
export function apply(ctx: Context) {
setTimeout(() => {
for (const runtime of ctx.registry.values()) {
for (const fiber of runtime.fibers) {
if (fiber.state === FiberState.PENDING) {
console.log(`${fiber.name} is PENDING — a required service is missing`)
}
}
}
}, 500)
}
```
再创建一个依赖无法满足的插件 `needs-timer.ts`
```ts
import type { Context } from 'cordis'
export const name = 'needs-timer'
export const inject = ['timer']
export function apply(ctx: Context) {
console.log('needs-timer loaded')
}
```
```yaml
- name: './needs-timer.ts'
- name: './diagnose.ts'
```
运行它(直接执行 `node --import tsx ../../vendor/cordis/bin.js`,按 Ctrl-C 停止):
```
needs-timer is PENDING — a required service is missing
```
`inject: ['timer']` 没有提供方。向列表添加 `- name: '@cordisjs/plugin-timer'` 后,插件就会加载。如果插件既不执行任何操作,也不报告任何内容,请检查其 fiber 状态。不加 PENDING 过滤条件进行迭代时,还会看到 loader 自身的插件Loader、Include处于 ACTIVE因为配置文件本身也是通过插件挂载的。
下一章:[进入 harness](07-into-the-harness.md):把相同模式用于真实的 harness 服务。
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-harness/deepseek-harness)

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
07-into-the-harness.md: 6ec42c50fe5059955734fe7bc46117538dafaffc
07-into-the-harness.zh.md: 32b21b008837e2972a53db9d893788dc6a7de9a9

View File

@@ -1,5 +1,7 @@
# 7. Into the harness
English | [中文](07-into-the-harness.zh.md)
This chapter registers a model-callable tool with the harness's `tools` service, executes it through the harness tool pipeline, and observes the result event. It remains keyless and does not call a model.
## A tool plugin

View File

@@ -0,0 +1,107 @@
# 7. 进入 harness
[English](07-into-the-harness.md) | 中文
本章会向 harness 的 `tools` 服务注册一个可由模型调用的工具,通过 harness 工具流水线执行它,并观察结果事件。整个示例无需密钥,也不会调用模型。
## 工具插件
创建 `greet-tool.ts`,将它放在 `tmp/cordis-tutorial` 中:
```ts
import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { CallId } from '@deepseek-ai/dsh-llm'
export const name = 'greet-tool'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'greet',
description: 'Greet the named person.',
parameters: {
name: { type: 'string', required: true, description: 'Who to greet' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args) {
return `Hello, ${args.name}!`
},
}))
// Drive one call through the real execution pipeline, standing in for
// the model. CallId brands the correlation id a provider would issue.
void (async () => {
const result = await ctx.tools.execute({
callId: CallId('demo-1'),
name: 'greet',
arguments: { name: 'Cordis' },
signal: new AbortController().signal,
})
console.log('tool replied:', JSON.stringify(result.content))
})()
}
```
这里的每个模式都来自前几章:`inject: ['tools']`[第 3 章](03-services.md))会让插件等待工具注册表就绪;`ctx.tools.register(...)` 会把注册 disposer 附着到插件([第 2 章](02-lifecycle-and-effects.md)),因此卸载时会注销工具。`defineTool``parameters` 规约转换为向模型展示的 JSON Schema推导 `args` 的类型,并在 `execute` 运行前校验模型提供的参数。工具返回由 `output.schema` 声明的规范值;`output.render` 则另行生成原生且持久的结果内容。
## 观察插件
创建 `tool-logger.ts`。这是一个独立插件,通过 harness 的 `tools/result` 事件观察应用中的每次工具调用:
```ts
import type { Context } from 'cordis'
import type {} from '@deepseek-ai/dsh-tools'
export const name = 'tool-logger'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.on('tools/result', (exec, result) => {
const text = result.content
.map(block => (block.type === 'text' ? block.text : ''))
.join('')
console.log(`[tool-logger] ${exec.name} -> ${text}`)
})
}
```
`import type {} from '@deepseek-ai/dsh-tools'` 行会引入该包的声明合并,使 `'tools/result'` 及其 payload 具有类型。这与第 4 章导入 `stats.ts` 的做法相同,只是扩展到了包级别。
## 组合并运行
```yaml
- name: '@deepseek-ai/dsh-system-prompt'
- name: '@deepseek-ai/dsh-tools'
- name: './tool-logger.ts'
- name: './greet-tool.ts'
```
`@deepseek-ai/dsh-tools` 会注入 `systemPrompt` 服务,因为工具需要向系统提示词贡献 schema所以组合中也要列出该服务的提供方。缺少提供方时工具插件会像[第 6 章](06-composition-and-hmr.md)所述那样保持 PENDING。
```sh
node --import tsx ../../vendor/cordis/bin.js
```
```
[tool-logger] greet -> Hello, Cordis!
tool replied: [{"type":"text","text":"Hello, Cordis!"}]
```
logger 会先触发:`tools/result` 在结果物化过程中发出,早于 `execute` 的 promise 向调用方返回结果。两个插件都不知道另一个插件存在,它们由注册表服务和事件连接。
## 从这里走向完整 agent智能体
真实 agent 就是这套组合再加上更多插件LLM大语言模型适配器、agent loop智能体循环、持久化和前端。对照 [examples/headless-agent/cordis.yml](../../examples/headless-agent/cordis.yml),你现在已经可以读懂其中每个配置项。将 `greet-tool.ts` 加入该文件的副本即可。
后续可以阅读:
- [构建工具](../user/develop/basic/tool.md):深入了解 `defineTool`,包括呈现和更丰富的 schema。
- [三层功能设计](../user/develop/practice/index.md)harness 如何组织可替换功能。
- 生成的[服务](../cordis-catalog/services.md)与[事件](../cordis-catalog/events.md)目录:可以注入和监听的所有内容。
- [架构](../architecture.md):这些插件所处的系统地图。
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-harness/deepseek-harness)

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
index.md: af622ad4e35829c6283c40f1b0019d7959dac973
index.zh.md: 35bad552ecce9c0496b0ed88b041a8109c81945b

View File

@@ -1,5 +1,7 @@
# Cordis tutorial
English | [中文](index.zh.md)
Cordis is the plugin framework underneath the DeepSeek Harness SDK: a small runtime where every capability — tools, LLM adapters, file access, the agent loop itself — is a plugin mounted into a shared context. This tutorial teaches Cordis hands-on: each chapter is a runnable example you build in a scratch directory inside this repository, ending with a plugin wired into real harness services.
The audience is agent developers. You do not need deep TypeScript experience; the [TypeScript notes](#typescript-notes) below explain the syntax that may be unfamiliar, and every chapter shows the exact commands and expected output.
@@ -41,6 +43,8 @@ That one-file launcher (see [vendor/cordis/bin.js](../../vendor/cordis/bin.js))
6. [Composition and HMR](06-composition-and-hmr.md) — the config file as a plugin tree, hot reload, and diagnosing a plugin that never loads.
7. [Into the harness](07-into-the-harness.md) — register a model-callable tool against real harness services.
<a id="typescript-notes"></a>
## TypeScript notes
The examples use three TypeScript features beyond ordinary modern JavaScript:

View File

@@ -0,0 +1,58 @@
# Cordis 教程
[English](index.md) | 中文
Cordis 是 DeepSeek Harness SDK 底层的插件框架它是一个小型运行时其中的每项能力包括工具、LLM大语言模型适配器、文件访问乃至 agent loop智能体循环本身都是挂载到共享上下文中的插件。本教程通过动手实践讲解 Cordis每一章都是一个可以运行的示例你将在本仓库内的临时目录中逐步构建它最后把一个插件接入真实的 harness 服务。
本教程面向 agent 开发者。你不需要深入掌握 TypeScript下文的 [TypeScript 说明](#typescript-notes)会解释可能陌生的语法,并且每一章都会给出确切命令和预期输出。
如果你想阅读精简的概念参考,而不是逐步实践,请参阅 [Cordis 入门](../cordis-primer.md)。详尽的 API 参考见生成的[事件](../cordis-catalog/events.md)与[服务](../cordis-catalog/services.md)目录,以及 [Cordis 核心 API](../cordis-catalog/core/context.md)页面。
## 准备工作
你需要克隆本仓库并安装依赖,具体前置条件见[快速入门](../user/guide/quickstart.md)。本教程不需要 API 密钥;所有示例均可在无密钥环境中运行。
```sh
git clone https://github.com/deepseek-harness/deepseek-harness.git
cd deepseek-harness
pnpm install
```
创建各章使用的临时目录。`tmp/` 已被 git 忽略,因此你在其中写入的任何内容都不会进入版本控制:
```sh
mkdir -p tmp/cordis-tutorial
cd tmp/cordis-tutorial
```
每一章都从该目录运行同一条命令:
```sh
node --import tsx ../../vendor/cordis/bin.js
```
这个单文件启动器(见 [vendor/cordis/bin.js](../../vendor/cordis/bin.js))会创建根 `Context`、挂载 Loader 插件,并让它从当前目录加载 `./cordis.yml`。其余所有内容,包括有哪些插件以及如何配置它们,都来自你稍后将编写的 YAML 文件。`--import tsx` 标志让 Node 无需构建步骤即可运行配置所指向的 TypeScript 文件。
## 章节
1. [你的第一个插件](01-first-plugin.md):插件是函数,由 loader 挂载。
2. [生命周期与 effect](02-lifecycle-and-effects.md):由 Cordis 管理的注册会在所属插件卸载时撤销。
3. [服务](03-services.md):在 `ctx` 上公开一项能力,并通过 `inject` 依赖它。
4. [事件](04-events.md):类型化事件、广播分发和 waterfall瀑布式事件的短路行为。
5. [配置](05-config.md):读取 `cordis.yml` 中经过校验的配置,并在输入错误时快速失败。
6. [组合与 HMR热模块替换](06-composition-and-hmr.md):把配置文件作为插件树,使用热重载,并诊断始终无法加载的插件。
7. [进入 harness](07-into-the-harness.md):基于真实的 harness 服务注册一个可由模型调用的工具。
<a id="typescript-notes"></a>
## TypeScript 说明
这些示例使用了普通现代 JavaScript 之外的三项 TypeScript 功能:
- **类型注解** 描述值,但不会改变运行时行为:`ctx: Context` 表示 `ctx` 具备 Cordis 上下文 API`who: string` 接受文本,而 `string[]` 表示字符串数组。
- **`import type { Context } from 'cordis'`** 只导入类型信息。它在运行时会消失,因此仅为类型注解使用 `Context` 的插件文件不会增加运行时依赖。
- **声明合并**`declare module 'cordis' { ... }`)会为 Cordis 已经声明的接口添加你的条目,例如新 `ctx.greeter` 属性的类型或事件名称。它不会生成任何运行时接线;插件必须另行提供服务或发出事件。第 3 章会完整展示该模式。
第 5 章还会使用 `interface` 描述配置对象的字段,并使用 `Schema<Config>` 这类泛型表示 schema 所校验的对象形状。你可以直接照写这些声明;周围的正文会解释每项声明连接了什么。
[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-harness/deepseek-harness)