fix(docs): align site with bilingual source pairs

This commit is contained in:
Yichen Jiang
2026-07-15 18:08:28 +08:00
parent e0a0b8b06d
commit 6af61c6f4e
43 changed files with 2112 additions and 353 deletions
@@ -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
events.md: 0c57681a55ea0200fe8f33293176fc94f09a4ce5
events.zh.md: 3e14739d4a97ba014d545c9f226000507aaeacef
+143
View File
@@ -0,0 +1,143 @@
# Event system
English | [中文](events.zh.md)
Events are the core communication mechanism between Cordis plugins. Harness uses them extensively for loosely coupled extension points.
## Basic use
### Listen for an event
```ts ignore-check
ctx.on('event-name', (payload) => {
// Handle the event.
})
```
### Emit an event
```ts ignore-check
ctx.emit('event-name', payload)
```
## Event modes
Cordis provides several event modes for different interaction contracts.
### emit — broadcast
Every listener runs synchronously and return values are ignored:
```ts ignore-check
// Emit
ctx.emit('my-plugin/ready', { id: 'worker-1' })
// Listen
ctx.on('my-plugin/ready', ({ id }) => {
console.log(`${id} is ready`)
})
```
### bail — short circuit
Listeners run in order; the first non-`undefined` result becomes the final result:
```ts ignore-check
// Dispatch
const result = ctx.bail('some-check', input)
// Listen: a returned value stops later listeners.
ctx.on('some-check', (input) => {
if (shouldBlock(input)) return 'blocked'
// Return undefined to continue to the next listener.
})
```
### serial — ordered execution
Listeners run in registration order and asynchronous results are awaited. The first listener to return a non-empty value stops further execution:
```ts ignore-check
await ctx.serial('setup-phase', context)
```
### waterfall — pipeline
Each listener may wrap the downstream result to form a processing chain. A listener **must call `next()` to delegate downstream**; omitting the call vetoes the pipeline:
```ts ignore-check
// Dispatch
const output = await ctx.waterfall('my-plugin/transform', input, async () => input)
// Listen: next() is mandatory.
ctx.on('my-plugin/transform', async (_input, next) => {
const downstream = await next()
return downstream.trim()
})
```
::: warning
A waterfall listener **must call `next()`**. Omitting it vetoes the pipeline by design, enabling interception and gateway behavior.
:::
## Typed events
Harness uses TypeScript declaration merging for type-safe events:
```ts
import 'cordis'
declare module 'cordis' {
interface Events {
'my-plugin/ready': (payload: { id: string }) => void
'my-plugin/check': (input: string) => boolean | undefined
'my-plugin/transform': (input: string, next: () => Promise<string>) => Promise<string>
}
}
// ctx.on('my-plugin/ready', ...) and ctx.emit('my-plugin/ready', ...)
// are now inferred correctly.
```
## Cordis events and session records
Harness Cordis events use `namespace/action` names, including `agent/pre-step`, `agent/request`, `agent/step-result`, `tools/result`, and `session/event`. The generated [event catalog](../../../cordis-catalog/events.md) records complete signatures and modes.
`turn/*`, `step/*`, `tool/call`, `tool/result`, and `compact/*` are durable session-event types, not same-named Cordis events. To observe them, listen to `session/event` and inspect `event.type`.
## Event listeners are effects
A listener registered with `ctx.on()` is removed automatically when its plugin unloads:
```ts ignore-check
export function apply(ctx: Context) {
// This listener is removed when the plugin disposes.
ctx.on('tools/result', handler)
}
```
## Example: logging plugin
This plugin logs tool calls and results:
```ts
import type { Context } from 'cordis'
import '@deepseek-ai/dsh-tools'
export const name = 'tool-logger'
export function apply(ctx: Context) {
ctx.on('tools/result', (exec, result) => {
console.log(`[tool] ${exec.name}(${JSON.stringify(exec.arguments)})`)
const text = result.content
.map(block => block.type === 'text' ? block.text : '')
.join('')
console.log(`[tool result] ${text.slice(0, 100)}`)
})
}
```
## Next steps
- [Capability layering](../practice/) — understand events within capability interfaces
- [LLM adapters](../practice/llm-adapter.md) — implement a complete LLM backend
+143
View File
@@ -0,0 +1,143 @@
# 事件系统
[English](events.md) | 中文
事件是 Cordis 插件间通信的核心机制。Harness 大量使用事件来实现松耦合的扩展点。
## 基本用法
### 监听事件
```ts ignore-check
ctx.on('event-name', (payload) => {
// Handle the event.
})
```
### 触发事件
```ts ignore-check
ctx.emit('event-name', payload)
```
## 事件模式
Cordis 提供多种事件触发模式,适用于不同场景:
### emit — 广播
所有监听器同步执行,不关心返回值:
```ts ignore-check
// Emit
ctx.emit('my-plugin/ready', { id: 'worker-1' })
// Listen
ctx.on('my-plugin/ready', ({ id }) => {
console.log(`${id} is ready`)
})
```
### bail — 短路
依次调用监听器,第一个返回非 `undefined` 值的结果作为最终值:
```ts ignore-check
// Dispatch
const result = ctx.bail('some-check', input)
// Listen: a returned value stops later listeners.
ctx.on('some-check', (input) => {
if (shouldBlock(input)) return 'blocked'
// Return undefined to continue to the next listener.
})
```
### serial — 顺序执行
监听器按注册顺序依次执行,并等待异步结果;第一个返回非空值的监听器会终止后续执行:
```ts ignore-check
await ctx.serial('setup-phase', context)
```
### waterfall — 管道
每个监听器可以包装下游返回值,形成处理链。**必须调用 `next()` 传递给下游**,不调用即为否决:
```ts ignore-check
// Dispatch
const output = await ctx.waterfall('my-plugin/transform', input, async () => input)
// Listen: next() is mandatory.
ctx.on('my-plugin/transform', async (_input, next) => {
const downstream = await next()
return downstream.trim()
})
```
::: warning
Waterfall 监听器**必须调用 `next()`**。不调用 `next` 等于否决整个管道,这是故意为之的设计——用于实现拦截/网关逻辑。
:::
## Typed Events
Harness 使用 TypeScript 声明合并来为事件提供类型安全:
```ts
import 'cordis'
declare module 'cordis' {
interface Events {
'my-plugin/ready': (payload: { id: string }) => void
'my-plugin/check': (input: string) => boolean | undefined
'my-plugin/transform': (input: string, next: () => Promise<string>) => Promise<string>
}
}
// ctx.on('my-plugin/ready', ...) and ctx.emit('my-plugin/ready', ...)
// are now inferred correctly.
```
## Cordis 事件与会话记录
Harness 的 Cordis 事件遵循 `namespace/action` 命名,例如 `agent/pre-step`、`agent/request`、`agent/step-result`、`tools/result` 和 `session/event`。完整签名与触发模式见[Events 目录](../../../cordis-catalog/events.md)。
`turn/*`、`step/*`、`tool/call`、`tool/result` 和 `compact/*` 是持久化的会话事件类型,不是同名 Cordis 事件。需要观察它们时,监听 `session/event` 并检查 `event.type`。
## 事件也是效果
通过 `ctx.on()` 注册的监听器会在插件卸载时自动移除:
```ts ignore-check
export function apply(ctx: Context) {
// This listener is removed when the plugin disposes.
ctx.on('tools/result', handler)
}
```
## 实战示例:日志插件
一个记录所有 tool 调用的简单插件:
```ts
import type { Context } from 'cordis'
import '@deepseek-ai/dsh-tools'
export const name = 'tool-logger'
export function apply(ctx: Context) {
ctx.on('tools/result', (exec, result) => {
console.log(`[tool] ${exec.name}(${JSON.stringify(exec.arguments)})`)
const text = result.content
.map(block => block.type === 'text' ? block.text : '')
.join('')
console.log(`[tool result] ${text.slice(0, 100)}`)
})
}
```
## 下一步
- [能力三件套](../practice/) — 事件在 capability seam 中的角色
- [LLM 适配器](../practice/llm-adapter.md) — 实现一个完整的 LLM 后端
@@ -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: bb08e7cb3f9d3a094100806451fd33d1482003cb
index.zh.md: 4b9d6d22c8d82bece67c71032a0028b21939f98f
+131
View File
@@ -0,0 +1,131 @@
# Plugins and lifecycle
English | [中文](index.zh.md)
This page describes the Cordis plugin model and lifecycle state machine.
## Fiber state machine
Every loaded plugin owns a **Fiber** scope with the following states:
```
PENDING → LOADING → ACTIVE
↘ FAILED
ACTIVE → UNLOADING → DISPOSED
```
| State | Meaning |
|------|------|
| PENDING | Declared, but required dependencies are not ready |
| LOADING | Dependencies are ready and `apply` is running |
| ACTIVE | The plugin is running |
| FAILED | `apply` threw an error |
| UNLOADING | The plugin is unloading and disposing resources |
| DISPOSED | The plugin is fully unloaded |
## Dependency-driven loading
A plugin with `inject` waits for every required service before loading:
```ts ignore-check
export const inject = ['tools', 'llm']
export function apply(ctx: Context) {
// ctx.tools and ctx.llm are ready here.
}
```
If a required service disappears, for example during provider replacement, the plugin unloads automatically (ACTIVE → DISPOSED) and loads again when the service returns.
## Automatic cleanup
Every registration made through `ctx` is undone when the plugin unloads:
```ts ignore-check
export function apply(ctx: Context) {
// Event listener: removed automatically on unload.
ctx.on('some-event', handler)
// Custom resource: the returned disposer runs on unload.
ctx.effect(() => {
const connection = createConnection()
return () => connection.close()
})
}
```
The framework tracks and disposes all of these operations:
- `ctx.on(event, handler)` — event listener
- `ctx.tools.register(tool)` — tool registration
- `ctx.llm.registerAdapter(names, adapter)` — LLM adapter registration
- `ctx.effect(() => cleanup)` — custom resource
During unload, disposer invocation starts in reverse registration order, but multiple async disposers run concurrently and have no serial completion guarantee. Put order-dependent cleanup in one disposer returned from a single `ctx.effect()` and await its steps serially there.
## Nested contexts
`ctx.plugin()` creates a child Fiber that inherits the parent context but has an independent lifecycle:
```ts ignore-check
export function apply(ctx: Context) {
// Register a child plugin.
ctx.plugin(childPlugin)
// The child has its own Fiber and unloads with its parent.
}
```
## Dispose semantics
To stop a plugin instance early:
```ts ignore-check
const fiber = ctx.plugin(myPlugin)
// Dispose it manually later.
fiber.dispose()
```
`dispose` guarantees:
1. All registrations owned by the plugin are removed.
2. Child plugins are recursively unloaded.
3. The returned promise resolves after all asynchronous cleanup finishes.
## Hot replacement (HMR)
With `@cordisjs/plugin-hmr` loaded from `cordis.yml`, editing a plugin source file triggers:
1. Unload the old plugin and clean up its registrations.
2. Load the new code.
3. Run the new `apply`.
Because plugin registrations clean themselves up, hot replacement does not retain registrations from the old instance.
## Example lifecycle
```ts ignore-check
export function apply(ctx: Context) {
console.log('plugin loading')
ctx.effect(() => {
console.log('effect registered')
return () => console.log('effect cleaned up')
})
}
```
Loading prints:
```
plugin loading
effect registered
```
Unloading prints:
```
effect cleaned up
```
## Next steps
- [Services and dependencies](./service.md) — expose a capability to other plugins
- [Event system](./events.md) — communicate between plugins
+131
View File
@@ -0,0 +1,131 @@
# 插件与生命周期
[English](index.md) | 中文
深入了解 Cordis 插件模型和生命周期状态机。
## Fiber 状态机
每个被加载的插件对应一个 **Fiber**(作用域)。Fiber 有以下状态:
```
PENDING → LOADING → ACTIVE
↘ FAILED
ACTIVE → UNLOADING → DISPOSED
```
| 状态 | 含义 |
|------|------|
| PENDING | 已声明但依赖未就绪 |
| LOADING | 依赖就绪,正在执行 `apply` |
| ACTIVE | 插件运行中 |
| FAILED | `apply` 抛出异常 |
| UNLOADING | 正在卸载,清理中 |
| DISPOSED | 已完全卸载 |
## 依赖驱动的加载
声明了 `inject` 的插件不会立即加载,而是等待依赖的服务就绪:
```ts ignore-check
export const inject = ['tools', 'llm']
export function apply(ctx: Context) {
// ctx.tools and ctx.llm are ready here.
}
```
如果依赖的服务消失(比如提供者被热替换),插件会被自动卸载(ACTIVE → DISPOSED),待服务恢复后重新加载。
## 自动清理机制
通过 `ctx` 做的任何注册,在插件卸载时都会自动撤销:
```ts ignore-check
export function apply(ctx: Context) {
// Event listener: removed automatically on unload.
ctx.on('some-event', handler)
// Custom resource: the returned disposer runs on unload.
ctx.effect(() => {
const connection = createConnection()
return () => connection.close()
})
}
```
以下操作都会被自动追踪和清理:
- `ctx.on(event, handler)` — 事件监听
- `ctx.tools.register(tool)` — tool 注册
- `ctx.llm.registerAdapter(names, adapter)` — LLM 适配器注册
- `ctx.effect(() => cleanup)` — 自定义资源
插件卸载时,处置器按注册顺序的反向发起,但多个异步处置器会并发执行,不保证逐个完成。存在顺序依赖的清理步骤必须放进同一个 `ctx.effect()` 返回的处置器中,由该处置器负责串行等待。
## 嵌套上下文
`ctx.plugin()` 创建子 Fiber,它继承父上下文但有独立的生命周期:
```ts ignore-check
export function apply(ctx: Context) {
// Register a child plugin.
ctx.plugin(childPlugin)
// The child has its own Fiber and unloads with its parent.
}
```
## dispose 语义
当你需要提前终止一个插件实例:
```ts ignore-check
const fiber = ctx.plugin(myPlugin)
// Dispose it manually later.
fiber.dispose()
```
`dispose` 保证:
1. 该插件注册的所有东西被撤销
2. 它的子插件也被递归卸载
3. 所有异步清理完成后 Promise resolve
## 热替换 (HMR)
在开发环境中(`cordis.yml` 加载了 `@cordisjs/plugin-hmr`),修改插件源文件会自动触发:
1. 卸载旧插件(清理所有注册)
2. 重新加载新代码
3. 执行新的 `apply`
因为所有注册都会被自动清理,所以热替换天然安全——不会留下旧状态。
## 实战:理解生命周期
```ts ignore-check
export function apply(ctx: Context) {
console.log('plugin loading')
ctx.effect(() => {
console.log('effect registered')
return () => console.log('effect cleaned up')
})
}
```
加载时输出:
```
plugin loading
effect registered
```
卸载时输出:
```
effect cleaned up
```
## 下一步
- [服务与依赖](./service.md) — 让你的插件对外提供能力
- [事件系统](./events.md) — 插件间通信的核心机制
@@ -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
service.md: 1bf28cb3c7dfdfbd6d0babfa3b1688ac65eea01e
service.zh.md: 17785c056ab9a0a21974e6ed8bbe7f7de05fa00e
+148
View File
@@ -0,0 +1,148 @@
# Services and dependencies
English | [中文](service.zh.md)
A service is a capability one plugin exposes to other plugins. `inject` declares the services a plugin requires.
## What is a service?
In Harness, `tools`, `llm`, and `agents` are services. Each is a named capability mounted on `ctx`:
```ts ignore-check
ctx.tools // ToolRegistry service
ctx.llm // LLM service
ctx.agents // Agent service
```
Any plugin can provide a service for other plugins to consume.
## Consume a service
Declare `inject` to use an existing service:
```ts ignore-check
export const inject = ['tools']
export function apply(ctx: Context) {
// ctx.tools exists and is ready here.
ctx.tools.register(/* ... */)
}
```
When `apply` runs, every service declared by `inject` is ready. If a service is not ready, the plugin waits instead of running.
## Provide a service
### Extend Service
```ts
import { Service, type Context } from 'cordis'
export default class MetricsService extends Service {
static inject = ['llm'] // A service may depend on other services.
constructor(ctx: Context) {
super(ctx, 'metrics') // 'metrics' is the service name.
}
// Public service method.
record(event: string, value: number) {
// ...
}
}
```
After loading this plugin, consumers access the service as `ctx.metrics`:
```ts ignore-check
export const inject = ['metrics']
export function apply(ctx: Context) {
ctx.metrics.record('tool_call', 1)
}
```
### Declare its type
Use TypeScript declaration merging to type `ctx.metrics`:
```ts
import { Service, type Context } from 'cordis'
declare module 'cordis' {
interface Context {
metrics: MetricsService
}
}
export default class MetricsService extends Service {
constructor(ctx: Context) {
super(ctx, 'metrics')
}
record(event: string, value: number) { /* ... */ }
}
```
## Dependency behavior
### Required and optional dependencies
```ts ignore-check
// Required: the plugin does not load while the service is absent.
export const inject = ['tools']
// Optional: omit inject and query with ctx.get() at the use site.
export function apply(ctx: Context) {
const metrics = ctx.get('metrics')
metrics?.record('plugin_loaded', 1)
}
```
### When a service disappears
If a required service disappears while the application is running, for example because its provider unloads:
1. Dependent plugins dispose automatically.
2. They load again when the service returns.
This prevents a plugin from calling a service that no longer exists.
## Service isolation
`cordis.yml` can isolate services so separate plugin groups see separate instances of the same service:
```yaml
- id: group-a
name: '@cordisjs/plugin-group'
group: true
isolate:
bash: true
config:
- name: '@deepseek-ai/dsh-bash-local'
config:
timeoutMs: 5000
- name: './src/plugin-a.ts'
- id: group-b
name: '@cordisjs/plugin-group'
group: true
isolate:
bash: true
config:
- name: '@deepseek-ai/dsh-bash-local'
config:
timeoutMs: 60000
- name: './src/plugin-b.ts'
```
`plugin-a` and `plugin-b` each see the Bash instance in their own group, with no cross-group effect.
## Built-in Harness services
The repository generates the service names, public methods, and source locations in the [service catalog](../../../cordis-catalog/services.md). Use that catalog and the service's TypeScript interface while developing a plugin; do not maintain a second static list.
## Next steps
- [Event system](./events.md) — communicate between plugins without tight coupling
- [Capability layering](../practice/) — use services as capability interfaces
+148
View File
@@ -0,0 +1,148 @@
# 服务与依赖
[English](service.md) | 中文
服务 (Service) 是插件对外暴露能力的方式。依赖 (inject) 是插件声明自己需要哪些服务。
## 什么是服务
在 Harness 中,`tools``llm``agents` 都是服务。服务是挂载在 `ctx` 上的命名能力:
```ts ignore-check
ctx.tools // ToolRegistry service
ctx.llm // LLM service
ctx.agents // Agent service
```
任何插件都可以提供一个新服务,供其他插件使用。
## 使用服务
声明 `inject` 来使用已有服务:
```ts ignore-check
export const inject = ['tools']
export function apply(ctx: Context) {
// ctx.tools exists and is ready here.
ctx.tools.register(/* ... */)
}
```
框架保证:在 `apply` 执行时,`inject` 声明的服务已经全部就绪。如果服务还没准备好,你的插件会等着,不会执行。
## 提供服务
### 使用 Service 基类
```ts
import { Service, type Context } from 'cordis'
export default class MetricsService extends Service {
static inject = ['llm'] // A service may depend on other services.
constructor(ctx: Context) {
super(ctx, 'metrics') // 'metrics' is the service name.
}
// Public service method.
record(event: string, value: number) {
// ...
}
}
```
加载这个插件后,其他插件就可以通过 `ctx.metrics` 访问它:
```ts ignore-check
export const inject = ['metrics']
export function apply(ctx: Context) {
ctx.metrics.record('tool_call', 1)
}
```
### 类型声明
使用 TypeScript 声明合并让 `ctx.metrics` 有正确类型:
```ts
import { Service, type Context } from 'cordis'
declare module 'cordis' {
interface Context {
metrics: MetricsService
}
}
export default class MetricsService extends Service {
constructor(ctx: Context) {
super(ctx, 'metrics')
}
record(event: string, value: number) { /* ... */ }
}
```
## 依赖的行为
### 必选依赖 vs 可选依赖
```ts ignore-check
// Required: the plugin does not load while the service is absent.
export const inject = ['tools']
// Optional: omit inject and query with ctx.get() at the use site.
export function apply(ctx: Context) {
const metrics = ctx.get('metrics')
metrics?.record('plugin_loaded', 1)
}
```
### 服务消失时的行为
如果一个必选依赖的服务在运行时消失(比如提供者被卸载):
1. 依赖它的插件自动 dispose
2. 当服务重新出现时,插件自动重新加载
这保证了不会出现"调用一个已不存在的服务"的情况。
## 服务隔离
`cordis.yml` 支持服务隔离——同一个服务可以有多个实例,不同插件组看到不同实例:
```yaml
- id: group-a
name: '@cordisjs/plugin-group'
group: true
isolate:
bash: true
config:
- name: '@deepseek-ai/dsh-bash-local'
config:
timeoutMs: 5000
- name: './src/plugin-a.ts'
- id: group-b
name: '@cordisjs/plugin-group'
group: true
isolate:
bash: true
config:
- name: '@deepseek-ai/dsh-bash-local'
config:
timeoutMs: 60000
- name: './src/plugin-b.ts'
```
`plugin-a` 和 `plugin-b` 各自看到自己组内的 bash 实例,互不影响。
## Harness 内置服务
服务名、公开方法和源码位置由仓库自动生成,见[服务目录](../../../cordis-catalog/services.md)。开发插件时应以该目录和服务接口的 TypeScript 类型为准,不要复制一份静态清单。
## 下一步
- [事件系统](./events.md) — 插件间松耦合通信
- [能力三件套](../practice/) — 服务在 seam 模式中的应用