feat(docs): build maintainable documentation site

This commit is contained in:
Yichen Jiang
2026-07-13 15:38:47 +08:00
parent 72bfba2d4d
commit d2f810e9fe
52 changed files with 2380 additions and 2224 deletions

View File

@@ -15,6 +15,7 @@ Every fact has exactly one home — the tier whose job it is — and every other
| [rfc/](rfc/README.md) | Decision records: the why and the what-was-given-up; `implemented/` RFCs describe shipped reality in present tense | Migration plans, test checklists, and spec-speak ("should…") once the decision has shipped |
| [postmortem/](postmortem/README.md) | Incident stories — the only tier where war-story narrative belongs | — |
| [cookbook/](cookbook/adding-a-package.md) | Step-by-step how-tos with numbered verify steps | Design rationale (→ the RFC each guide links) |
| [user/](user/zh-CN/index.md) | Product-facing guides published by the documentation website | Generated reference tables, contributor procedures, decision history |
| Package README | The per-package contract: config, semantics, limitations, extension points | JSDoc restatement, generated-catalog restatement (event/tool tables), other packages' concerns |
| [development.md](development.md) | First-stop contributor onboarding: local setup, daily workflow, and CI shape at summary level; a bilingual pair under the [i18n contract](i18n/README.md) | Runtime/version rationale (→ RFCs), gate-by-gate enumerations that drift from `package.json` scripts |
| Generated catalogs: [cordis events](cordis-catalog/events.md), [cordis services](cordis-catalog/services.md), [tool-catalog](tool-catalog.md), [config-catalog](config-catalog.md), [persistence-catalog](persistence-catalog.md), [module-graph.md](module-graph.md) | Exhaustive enumerations regenerated from source, freshness-gated | Hand edits of any kind |

View File

@@ -160,6 +160,7 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand;
| [Raise the Node LTS engine floor to 22.19](implemented/process/2026-07-06-node-engine-floor.md) | 2026-07-06 |
| [Parallel GitHub CI gates](implemented/process/2026-07-06-parallel-github-ci-gates.md) | 2026-07-06 |
| [Parallel pre-push gates](implemented/process/2026-07-06-parallel-pre-push-gates.md) | 2026-07-06 |
| [Project canonical documentation into the website](implemented/process/2026-07-13-documentation-site-projection.md) | 2026-07-13 |
### Testing

View File

@@ -0,0 +1,39 @@
# RFC: Project canonical documentation into the website
Status: implemented
## Problem
The repository needs a navigable documentation website without turning the website directory into a second documentation source. Copying package guides, architecture pages, or generated catalogs into a site-specific tree allows the two copies to drift, while pointing VitePress directly at the repository root couples public URLs and navigation to the internal file layout. Repository-relative links also need different destinations on the website: published pages stay inside the site, but source files and unpublished contributor documents belong on GitHub.
## Decision
Canonical Markdown remains in the repository tier that owns it. Product-facing guides live under `docs/user/`, generated reference remains in the existing generated catalogs, and architectural and cookbook pages remain at their existing `docs/` paths.
`website/docs.ts` is an explicit publication manifest. Each entry maps one canonical source file to a stable public route, sidebar, section, and order. Adding or removing a published page is therefore a reviewable manifest change rather than an implicit directory crawl.
`scripts/project-doc-site.ts` projects the manifest into the ignored `website/.generated/` directory before VitePress starts or builds. The generated tree follows public routes so VitePress navigation, locale detection, and local search share the same route vocabulary. Each page receives an `editSource` frontmatter field pointing to its canonical repository file; the edit-link callback reads only that page data, so public URLs remain independent of the source layout.
The projector parses Markdown links without reserializing the document. A link to another published source becomes a site-relative route; a link to an unpublished repository file becomes a GitHub source link; a repository image becomes a raw GitHub URL. Missing relative targets fail projection. Unit tests pin these transformations, and `docs:check` runs the projector tests plus a production VitePress build as part of `doc-sync` and the parallel documentation gates.
Mermaid renders the canonical diagrams. The website workspace explicitly declares the five packages that `vitepress-plugin-mermaid` asks Vite to prebundle because pnpm's strict dependency isolation otherwise makes those transitive packages unavailable to the local development server; Knip records this runtime-only use as an intentional dependency exception.
Site publication is separate from site construction. The repository contains local development and build commands, but no hosting or deployment workflow until a public destination is chosen.
## Alternatives considered
**Commit copied Markdown under `website/`.** This makes VitePress setup direct, but every copied guide or API table gains two owners and requires a synchronization convention that cannot identify which copy is authoritative.
**Make `website/` the canonical home for every published page.** This keeps one copy but moves architecture, generated reference, and contributor-facing material away from their repository ownership tiers merely to satisfy a renderer.
**Discover every Markdown file automatically.** This minimizes manifest maintenance but publishes internal documents accidentally, exposes source moves as URL changes, and produces navigation from incidental directory order.
**Use filesystem symlinks.** Symlinks preserve a single source but do not solve public routing or repository-relative links, and their behavior is less predictable across local development, package tooling, and hosted CI environments.
**Build only in a deployment workflow.** A deployment job can reveal rendering failures after merge. Keeping the production build in `doc-sync` makes the same failure visible locally and in ordinary CI even when no public deployment exists.
## Consequences
Documentation facts have one editable home, public routes remain stable across source moves, and the site can include generated references without committing another generated copy. Local development watches canonical inputs and regenerates the disposable projection.
The publication manifest is a maintained allowlist, and link projection adds a small repository-specific build adapter. A new kind of Markdown link behavior needs a projector test. Mermaid support also increases the client bundle size, but preserves diagrams already used by the canonical documentation.

View File

@@ -0,0 +1,108 @@
# 插件配置
让你的插件接受用户在 `cordis.yml` 中传入的配置。
## 定义 Config 类型
在插件中导出一个 `Config` 类型和可选的默认值:
```typescript
import type { Context } from 'cordis'
export const name = 'my-plugin'
export interface Config {
greeting: string
maxRetries: number
verbose?: boolean
}
export const Config = {
greeting: 'Hello',
maxRetries: 3,
verbose: false,
}
export function apply(ctx: Context, config: Config) {
console.log(config.greeting) // 用户配置或默认值
}
```
用户在 `cordis.yml` 中这样使用:
```yaml
- name: './src/my-plugin.ts'
config:
greeting: 'Hi there'
maxRetries: 5
```
未提供的字段使用导出的 `Config` 对象中的默认值。
## Schema 校验
对于需要严格校验的场景,使用 Schemastery 定义 schema
```typescript
import type { Context } from 'cordis'
import Schema from 'schemastery'
export const name = 'validated-plugin'
export interface Config {
apiKey: string
timeout: number
mode: 'fast' | 'accurate'
}
export const Config = Schema.object({
apiKey: Schema.string().required(),
timeout: Schema.number().default(30000),
mode: Schema.union(['fast', 'accurate']).default('fast'),
})
export function apply(ctx: Context, config: Config) {
// config 已经过校验,类型安全
}
```
Schema 在插件加载时执行校验。如果配置不合法,插件会加载失败并给出明确错误信息。
## 设计原则
### 无硬编码可调参数
Harness 的约定:**任何两个部署可能想要不同值的东西,都应该是配置字段**。
```typescript
// 错误 — 硬编码超时时间
const TIMEOUT = 30000
// 正确 — 可配置
export interface Config {
timeoutMs: number // 默认 30000
}
```
检验标准:能否在 `cordis.yml` 中改变这个值,而不需要修改代码?
### 配置错误要响亮
如果配置引用了不存在的东西(比如一个不存在的模型名),应该尽早报错,而不是静默跳过:
```typescript
export function apply(ctx: Context, config: Config) {
if (!ctx.llm.hasAdapter(config.model)) {
throw new Error(`Model "${config.model}" is not registered by any LLM adapter`)
}
}
```
## 配合 HMR
配置变更会触发插件热替换:修改 `cordis.yml` 中某个插件的 `config`,框架会卸载旧实例、加载新实例。由于注册都是效果(自动清理),这个过程是安全的。
## 下一步
- [插件与生命周期](../framework/) — 深入了解插件的完整生命周期
- [服务与依赖](../framework/service.md) — 让你的插件对外提供服务

View File

@@ -0,0 +1,148 @@
# 第一个插件
本文带你编写一个最小的 Harness 插件并加载到 Agent 中。
## 插件是什么
在 Harness 中,插件是一个导出 `apply` 函数的 TypeScript 模块。框架在加载时调用 `apply`,传入一个 `ctx`(上下文对象),你通过 `ctx` 注册能力:
```typescript
import type { Context } from 'cordis'
export const name = 'my-plugin'
export function apply(ctx: Context) {
// 在这里注册能力
}
```
就这么简单。
## 创建插件文件
在你的项目目录下创建 `src/my-plugin.ts`
```typescript
import type { Context } from 'cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) {
// 监听 agent-loop 的 ready 事件
ctx.on('ready', () => {
console.log('[hello-plugin] 插件已加载!')
})
}
```
## 注册到 cordis.yml
在你的 `cordis.yml` 中添加一条:
```yaml
- id: hello
name: './src/my-plugin.ts'
```
启动后你会在控制台看到 `[hello-plugin] 插件已加载!`
## 自动清理
通过 `ctx` 注册的任何东西——事件监听、tool、定时器——在插件卸载时都会被自动清理。你不需要手动 removeListener 或 clearInterval。
如果你有需要手动清理的资源(比如一个网络连接),用 `ctx.effect()` 告诉框架怎么清理:
```typescript
export function apply(ctx: Context) {
ctx.effect(() => {
const timer = setInterval(() => {
console.log('heartbeat')
}, 5000)
// 返回的函数会在插件卸载时被调用
return () => clearInterval(timer)
})
}
```
## 声明依赖
如果你的插件需要使用其他服务(如 `tools``llm`),需要声明 `inject`
```typescript
export const name = 'my-tool-plugin'
export const inject = ['tools']
export function apply(ctx: Context) {
// ctx.tools 现在可用
ctx.tools.register(/* ... */)
}
```
框架会确保依赖的服务就绪后才加载你的插件。
## 插件的三种形态
除了函数形式,插件还支持对象形式和类形式:
### 对象形式
```typescript
export default {
name: 'my-plugin',
inject: ['tools'],
apply(ctx: Context) {
// ...
},
}
```
### 类形式
```typescript
import { Service } from 'cordis'
export default class MyService extends Service {
static inject = ['tools']
constructor(ctx: Context) {
super(ctx, 'myService')
}
start() {
// 服务启动逻辑
}
}
```
大多数情况下,函数形式足够了。类形式用于需要对外提供服务的插件(见 [服务与依赖](../framework/service.md))。
## 完整示例
参考仓库中的 `examples/echo-agent/src/echo-tool.ts`,这是一个注册 tool 的插件:
```typescript
import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'echo-tool'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'echo',
description: 'Echo the given text back, uppercased.',
parameters: {
text: { type: 'string', required: true },
},
async execute(args) {
return [{ type: 'text', text: `ECHO: ${args.text.toUpperCase()}` }]
},
}))
}
```
## 下一步
- [开发一个 Tool](./tool.md) — 详细了解 tool 定义 DSL
- [插件配置](./config.md) — 让插件接受用户配置

View File

@@ -0,0 +1,199 @@
# 开发一个 Tool
Tool 是模型可以调用的能力。本文介绍如何用 `defineTool` 编写一个 tool。
## 最小示例
```typescript
import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'my-tool'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'greet',
description: 'Greet someone by name.',
parameters: {
name: { type: 'string', required: true, description: 'The name to greet' },
},
async execute(args) {
// args 自动推导为 { name: string }
return [{ type: 'text', text: `Hello, ${args.name}!` }]
},
}))
}
```
## 参数定义
`parameters` 用一种简洁的格式描述参数,框架会自动转换为模型需要的 JSON Schema。
### 基本类型
```typescript
parameters: {
path: { type: 'string', required: true },
limit: { type: 'number' },
recursive: { type: 'boolean' },
}
// 推导类型: { path: string; limit?: number; recursive?: boolean }
```
### 枚举
```typescript
parameters: {
mode: { type: 'string', required: true, enum: ['read', 'write', 'append'] },
}
// 推导类型: { mode: string } (运行时校验 enum 值)
```
### 嵌套对象
```typescript
parameters: {
options: {
type: 'object',
properties: {
timeout: { type: 'number' },
retries: { type: 'number' },
},
},
}
// 推导类型: { options?: { timeout?: number; retries?: number } }
```
### 数组
```typescript
parameters: {
tags: {
type: 'array',
items: { type: 'string' },
},
}
// 推导类型: { tags?: string[] }
```
### 每个属性的字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `type` | `'string' \| 'number' \| 'boolean' \| 'object' \| 'array'` | 值类型 |
| `required` | `true` | 标记为必填(影响类型推导) |
| `description` | `string` | 发送给模型的描述 |
| `enum` | `string[]` | 允许的枚举值 |
| `properties` | `SchemaSpec` | 嵌套属性type 为 object 时) |
| `items` | `SchemaProp` | 数组元素 schematype 为 array 时) |
## execute 函数
`execute` 接收经过校验的 `args`(类型自动推导)和一个 `exec` 上下文对象:
```typescript
async execute(args, exec) {
// args: 根据 parameters 自动推导的类型
// exec: ToolExecution 对象,提供执行上下文
// 返回 ContentBlock 数组
return [{ type: 'text', text: 'result here' }]
}
```
### 返回值
`execute` 必须返回一个 `ContentBlock[]`,告诉模型 tool 的执行结果:
```typescript
// 文本结果
return [{ type: 'text', text: 'file content here...' }]
// 多个 block
return [
{ type: 'text', text: 'Found 3 matches:' },
{ type: 'text', text: matchResults.join('\n') },
]
```
### 参数校验
`defineTool` 在调用 `execute` 之前会自动校验模型生成的参数。如果参数不合法,会抛出 `ToolArgsError`,框架将其转换为 `isError` 结果返回给模型,让模型自行修正。
你不需要在 `execute` 里手动校验参数类型。
## 展示层 (Presentation)
Tool 可以定义 UI 渲染方法,用于在终端或 ACP 客户端中展示 tool call 和 result
```typescript
defineTool({
name: 'bash',
// ...
presentCall(args) {
return {
intent: 'terminal',
title: `bash(${JSON.stringify(args.command).slice(0, 60)})`,
}
},
presentResult(args, result) {
return {
intent: 'terminal',
body: result.content.map(b => b.type === 'text' ? b.text : '').join(''),
}
},
})
```
`presentCall``presentResult` 是**纯函数**不能有副作用——UI 可能在流式传输中和会话回放中多次调用它们。
## 注册与卸载
`ctx.tools.register()` 返回值就是 disposer。但由于你在 `ctx` 上调用,框架已经自动追踪了这个注册——插件卸载时会自动移除 tool。你不需要手动调用 disposer。
```typescript
// 这样就够了:
ctx.tools.register(defineTool({ /* ... */ }))
// 不需要:
// const dispose = ctx.tools.register(...)
// ctx.on('dispose', dispose)
```
## 完整实战示例
一个文件计数 tool
```typescript
import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { readdir } from 'node:fs/promises'
export const name = 'file-counter'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'count_files',
description: 'Count files in a directory.',
parameters: {
path: { type: 'string', required: true, description: 'Directory path' },
extension: { type: 'string', description: 'Filter by extension (e.g. ".ts")' },
},
async execute(args) {
const entries = await readdir(args.path, { withFileTypes: true })
let files = entries.filter(e => e.isFile())
if (args.extension) {
files = files.filter(f => f.name.endsWith(args.extension!))
}
return [{ type: 'text', text: `Found ${files.length} files.` }]
},
}))
}
```
## 下一步
- [插件配置](./config.md) — 让你的 tool 可配置
- [能力三件套](../practice/) — 了解 seam/impl/consumer 模式

View File

@@ -0,0 +1,152 @@
# 事件系统
事件是 Cordis 插件间通信的核心机制。Harness 大量使用事件来实现松耦合的扩展点。
## 基本用法
### 监听事件
```typescript
ctx.on('event-name', (payload) => {
// 处理事件
})
```
### 触发事件
```typescript
ctx.emit('event-name', payload)
```
## 事件模式
Cordis 提供多种事件触发模式,适用于不同场景:
### emit — 广播
所有监听器并行执行,不关心返回值:
```typescript
// 触发
ctx.emit('agent/turn-end', { agentId, turnIndex })
// 监听
ctx.on('agent/turn-end', ({ agentId, turnIndex }) => {
console.log(`Turn ${turnIndex} ended`)
})
```
### bail — 短路
依次调用监听器,第一个返回非 `undefined` 值的结果作为最终值:
```typescript
// 触发
const result = ctx.bail('some-check', input)
// 监听(返回值阻止后续监听器)
ctx.on('some-check', (input) => {
if (shouldBlock(input)) return 'blocked'
// 返回 undefined 继续传递给下一个监听器
})
```
### serial — 顺序执行
所有监听器按注册顺序依次执行(异步安全):
```typescript
await ctx.serial('setup-phase', context)
```
### waterfall — 管道
每个监听器接收前一个的输出,形成数据管道。**必须调用 `next()` 传递给下游**,不调用即为否决:
```typescript
// 触发
const finalMessages = await ctx.waterfall('llm/pre-request', messages)
// 监听(必须调用 next
ctx.on('llm/pre-request', async (messages, next) => {
// 可以修改 messages
messages.push(extraMessage)
// 必须调用 next() 传递给下一个监听器
return next(messages)
})
```
::: warning
Waterfall 监听器**必须调用 `next()`**。不调用 `next` 等于否决整个管道,这是故意为之的设计——用于实现拦截/网关逻辑。
:::
## Typed Events
Harness 使用 TypeScript 声明合并来为事件提供类型安全:
```typescript
declare module 'cordis' {
interface Events {
'my-plugin/ready': (payload: { id: string }) => void
'my-plugin/check': (input: string) => boolean | undefined
}
}
// 现在 ctx.on('my-plugin/ready', ...) 和 ctx.emit('my-plugin/ready', ...)
// 都有正确的类型推导
```
## 命名约定
Harness 事件遵循 `namespace/action` 命名:
```
agent/pre-step — agent 执行一步之前
agent/post-step — agent 执行一步之后
tool/call — tool 被调用
tool/result — tool 返回结果
llm/pre-request — LLM 请求发送前
session/event — 会话事件被记录
compact/start — 压缩开始
compact/end — 压缩结束
```
## 事件也是效果
通过 `ctx.on()` 注册的监听器会在插件卸载时自动移除:
```typescript
export function apply(ctx: Context) {
// 这个监听器在插件 dispose 时自动清理
ctx.on('agent/turn-end', handler)
}
```
## 实战示例:日志插件
一个记录所有 tool 调用的简单插件:
```typescript
import type { Context } from 'cordis'
export const name = 'tool-logger'
export function apply(ctx: Context) {
ctx.on('tool/call', ({ name, args }) => {
console.log(`[tool] ${name}(${JSON.stringify(args)})`)
})
ctx.on('tool/result', ({ name, result }) => {
const text = result.content
.filter(b => b.type === 'text')
.map(b => b.text)
.join('')
console.log(`[tool result] ${text.slice(0, 100)}`)
})
}
```
## 下一步
- [能力三件套](../practice/) — 事件在 capability seam 中的角色
- [LLM 适配器](../practice/llm-adapter.md) — 实现一个完整的 LLM 后端

View File

@@ -0,0 +1,139 @@
# 插件与生命周期
深入了解 Cordis 插件模型和生命周期状态机。
## Fiber 状态机
每个被加载的插件对应一个 **Fiber**作用域。Fiber 有以下状态:
```
PENDING → LOADING → ACTIVE
↘ FAILED
ACTIVE → UNLOADING → DISPOSED
```
| 状态 | 含义 |
|------|------|
| PENDING | 已声明但依赖未就绪 |
| LOADING | 依赖就绪,正在执行 `apply` |
| ACTIVE | 插件运行中 |
| FAILED | `apply` 抛出异常 |
| UNLOADING | 正在卸载,清理中 |
| DISPOSED | 已完全卸载 |
## 依赖驱动的加载
声明了 `inject` 的插件不会立即加载,而是等待依赖的服务就绪:
```typescript
export const inject = ['tools', 'llm']
export function apply(ctx: Context) {
// 到这里时ctx.tools 和 ctx.llm 一定存在
}
```
如果依赖的服务消失比如提供者被热替换插件会被自动卸载ACTIVE → DISPOSED待服务恢复后重新加载。
## 自动清理机制
通过 `ctx` 做的任何注册,在插件卸载时都会自动撤销:
```typescript
export function apply(ctx: Context) {
// 事件监听——卸载时自动移除
ctx.on('some-event', handler)
// 自定义资源——卸载时调用返回的函数
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.plugin()` 创建子 Fiber它继承父上下文但有独立的生命周期
```typescript
export function apply(ctx: Context) {
// 注册一个子插件
ctx.plugin(childPlugin)
// 子插件有自己的 Fiber父卸载时子也卸载
}
```
## dispose 语义
当你需要提前终止一个插件实例:
```typescript
const fiber = ctx.plugin(myPlugin)
// 之后可以手动 dispose
fiber.dispose()
```
`dispose` 保证:
1. 该插件注册的所有东西被撤销
2. 它的子插件也被递归卸载
3. 所有异步清理完成后 Promise resolve
## 热替换 (HMR)
在开发环境中(`cordis.yml` 加载了 `@cordisjs/plugin-hmr`),修改插件源文件会自动触发:
1. 卸载旧插件(清理所有注册)
2. 重新加载新代码
3. 执行新的 `apply`
因为所有注册都会被自动清理,所以热替换天然安全——不会留下旧状态。
## 实战:理解生命周期
```typescript
export function apply(ctx: Context) {
console.log('plugin loading')
ctx.on('ready', () => {
console.log('context ready')
})
ctx.on('dispose', () => {
console.log('plugin disposing')
})
ctx.effect(() => {
console.log('effect registered')
return () => console.log('effect cleaned up')
})
}
```
加载时输出:
```
plugin loading
effect registered
context ready
```
卸载时输出(逆序):
```
plugin disposing
effect cleaned up
```
## 下一步
- [服务与依赖](./service.md) — 让你的插件对外提供能力
- [事件系统](./events.md) — 插件间通信的核心机制

View File

@@ -0,0 +1,137 @@
# 服务与依赖
服务 (Service) 是插件对外暴露能力的方式。依赖 (inject) 是插件声明自己需要哪些服务。
## 什么是服务
在 Harness 中,`tools``llm``agents` 都是服务。服务是挂载在 `ctx` 上的命名能力:
```typescript
ctx.tools // ToolRegistry 服务
ctx.llm // LLM 服务
ctx.agents // Agent 服务
```
任何插件都可以提供一个新服务,供其他插件使用。
## 使用服务
声明 `inject` 来使用已有服务:
```typescript
export const inject = ['tools']
export function apply(ctx: Context) {
// ctx.tools 在这里一定存在且就绪
ctx.tools.register(/* ... */)
}
```
框架保证:在 `apply` 执行时,`inject` 声明的服务已经全部就绪。如果服务还没准备好,你的插件会等着,不会执行。
## 提供服务
### 使用 Service 基类
```typescript
import { Service, type Context } from 'cordis'
export default class MetricsService extends Service {
static inject = ['llm'] // 本服务也可以依赖其他服务
constructor(ctx: Context) {
super(ctx, 'metrics') // 'metrics' 是服务名
}
// 服务的公开方法
record(event: string, value: number) {
// ...
}
}
```
加载这个插件后,其他插件就可以通过 `ctx.metrics` 访问它:
```typescript
export const inject = ['metrics']
export function apply(ctx: Context) {
ctx.metrics.record('tool_call', 1)
}
```
### 类型声明
使用 TypeScript 声明合并让 `ctx.metrics` 有正确类型:
```typescript
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 可选依赖
```typescript
// 必选:服务不存在时,插件不会加载
export const inject = ['tools']
// 可选:服务不存在时,插件仍然加载,但 ctx.xxx 可能是 undefined
export const inject = { optional: ['metrics'] }
```
### 服务消失时的行为
如果一个必选依赖的服务在运行时消失(比如提供者被卸载):
1. 依赖它的插件自动 dispose
2. 当服务重新出现时,插件自动重新加载
这保证了不会出现"调用一个已不存在的服务"的情况。
## 服务隔离
`cordis.yml` 支持服务隔离——同一个服务可以有多个实例,不同插件组看到不同实例:
```yaml
- id: group-a
name: 'group:'
config:
- name: '@deepseek-ai/dsh-bash-local'
config:
timeoutMs: 5000
- name: './src/plugin-a.ts'
- id: group-b
name: 'group:'
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 模式中的应用

View File

@@ -0,0 +1,156 @@
# 能力的三层拆分
当一个能力(插件)足够通用(比如"执行 bash 命令"Harness 会把它拆成三个包:**接口**、**实现**、**消费者**。这样可以独立替换其中任何一层。
## 以 Bash 为例
考虑 "Bash 执行" 这个能力:
- **接口** (`dsh-bash`) — 定义"bash 执行"长什么样:输入是什么、输出是什么
- **实现** (`dsh-bash-local`) — 真正在本地跑命令的代码
- **消费者** (`dsh-tool-bash`) — 把这个能力包装成模型能调用的 tool
```
┌─────────────┐ ┌──────────────────┐ ┌──────────────┐
│ dsh-bash │────▶│ dsh-bash-local │ │ dsh-tool-bash│
│ (接口) │ │ (实现) │ │ (消费者/tool)│
└─────────────┘ └──────────────────┘ └──────────────┘
▲ │
└────────────────────────────────────────────┘
inject: ['bash']
```
## 拆分的好处
### 具体实现可替换
同一个接口可以有多种实现。用户通过 `cordis.yml` 选择:
```yaml
# 本地执行
- name: '@deepseek-ai/dsh-bash-local'
# 或:远程沙箱执行(未来)
# - name: '@deepseek-ai/dsh-bash-remote'
# config:
# endpoint: 'https://sandbox.example.com'
```
接口不变、tool 不变,只换实现。
### 独立演进
- 接口定义稳定后很少改动
- 实现可以独立优化(性能、安全)
- 消费者tool可以调整对模型的呈现方式
### 依赖解耦
- 实现 depend on 接口
- 消费者 depend on 接口
- 实现和消费者**互不依赖**
## Harness 中内置的三件套
| 能力 | 接口 (seam) | 实现 | 消费者 (tool) |
|------|-------------|------|---------------|
| Bash | `dsh-bash` | `dsh-bash-local` | `dsh-tool-bash` |
| 文件系统 | `dsh-fs` | `dsh-fs-local` + `dsh-fs-policy` | `dsh-tool-fs` |
| Web | `dsh-web` | `dsh-web-fetch-local` / `dsh-web-search-*` | `dsh-tool-web` |
| 子代理 | `dsh-subagent` | `dsh-subagent-spawn` / `dsh-subagent-fork` | `dsh-tool-subagent` |
| 压缩 | `dsh-compact` | `dsh-compact-basic` | 由实现插件消费 agent-loop 的扩展事件 |
## 开发你自己的三件套
### 第一步:定义接口
```typescript
// packages/my-cap/my-cap/src/index.ts
import { Service, type Context } from 'cordis'
declare module 'cordis' {
interface Context {
myCap: MyCapService
}
}
export abstract class MyCapService extends Service {
constructor(ctx: Context) {
super(ctx, 'myCap')
}
/** 执行能力的核心方法 */
abstract execute(request: MyCapRequest): Promise<MyCapResult>
}
export interface MyCapRequest {
input: string
}
export interface MyCapResult {
output: string
}
```
### 第二步:编写实现
```typescript
// packages/my-cap/my-cap-local/src/index.ts
import type { Context } from 'cordis'
import { MyCapService, type MyCapRequest, type MyCapResult } from '@deepseek-ai/dsh-my-cap'
class MyCapLocal extends MyCapService {
async execute(request: MyCapRequest): Promise<MyCapResult> {
// 具体实现
return { output: request.input.toUpperCase() }
}
}
export const name = 'my-cap-local'
export function apply(ctx: Context) {
ctx.plugin(MyCapLocal)
}
```
### 第三步:编写消费者 (tool)
```typescript
// packages/my-cap/tool-my-cap/src/index.ts
import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'tool-my-cap'
export const inject = ['tools', 'myCap']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'my_cap',
description: 'Execute my capability.',
parameters: {
input: { type: 'string', required: true },
},
async execute(args) {
const result = await ctx.myCap.execute({ input: args.input })
return [{ type: 'text', text: result.output }]
},
}))
}
```
### 在 cordis.yml 中组合
```yaml
- name: '@deepseek-ai/dsh-my-cap-local'
- name: '@deepseek-ai/dsh-tool-my-cap'
```
## 设计要点
- **不要预防性拆分** — 只有当你确实需要可替换实现时才拆三件套。一个简单的 tool 插件不需要拆分。
- **接口定义 Request/Result 类型** — 实现和消费者只依赖接口包。
- **Explicit > Implicit** — 实现中的默认值处理应该是显式的 `resolve(request): Spec` 步骤,不是隐藏在 `run()` 中的 `?? default`
## 下一步
- [LLM 适配器](./llm-adapter.md) — 实现一个 LLM 后端(最常见的 seam 扩展)

View File

@@ -0,0 +1,169 @@
# LLM 适配器
本文介绍如何为 Harness 接入一个新的 LLM 提供方。
## 概述
LLM 适配器是一个继承 `LlmAdapter` 的类,实现 `stream()` 方法,将 Harness 的统一请求格式转换为具体 API 的调用。
## 最小实现
```typescript
import type { Context } from 'cordis'
import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'
class MyAdapter extends LlmAdapter {
private apiKey: string
constructor(apiKey: string) {
super()
this.apiKey = apiKey
}
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
// 1. 将 options.messages 转换为你的 API 格式
// 2. 调用 API流式
// 3. 将 API 响应转换为 StreamChunk 序列
}
}
export interface Config {
apiKey: string
models: string[]
}
export const name = 'my-llm-adapter'
export const inject = ['llm']
export function apply(ctx: Context, config: Config) {
const adapter = new MyAdapter(config.apiKey)
ctx.llm.registerAdapter(config.models, adapter)
}
```
## StreamChunk 协议
`stream()` 必须按以下协议 yield chunk
```typescript
// 1. 每个内容块以 block-start 开始
yield { type: 'block-start', index: 0, blockType: 'text' }
// 2. 文本块使用 text-delta
yield { type: 'text-delta', index: 0, text: 'Hello' }
yield { type: 'text-delta', index: 0, text: ' world' }
// 3. 每个内容块以 block-end 结束(携带完整 block
yield {
type: 'block-end',
index: 0,
block: { type: 'text', text: 'Hello world' },
}
// 4. Tool call 块
yield { type: 'block-start', index: 1, blockType: 'tool-call' }
yield {
type: 'tool-call-delta',
index: 1,
id: CallId('call-123'),
name: 'bash',
argumentsDelta: '{"command":"ls"}',
}
yield {
type: 'block-end',
index: 1,
block: {
type: 'tool-call',
id: CallId('call-123'),
name: 'bash',
arguments: '{"command":"ls"}',
},
}
// 5. Token 用量
yield { type: 'usage', usage: { inputTokens: 100, outputTokens: 50 } }
// 6. 结束原因
yield { type: 'finish', reason: { kind: 'stop' } }
// 或: { kind: 'tool-calls' } 表示模型想调用 tool
```
### 关键规则
- 每个 `block-start` 必须有对应的 `block-end`
- `index` 从 0 递增,标识内容块顺序
- `tool-call-delta``argumentsDelta` 是 JSON 字符串的增量(可以一次 yield 全部,也可以分多次)
- `finish` 必须是最后一个 chunk
- `usage``finish` 之前 yield
## GenerateOptions
`stream()` 接收的请求包含:
```typescript
interface GenerateOptions {
/** 模型名 */
model: string
/** 对话历史 */
messages: Message[]
/** 可用的 tool 列表 */
tools?: ToolSpec[]
/** 系统提示词 */
system?: string
/** 最大输出 token */
maxTokens?: number
/** 温度 */
temperature?: number
}
```
你的适配器需要将这些映射到具体 API 的参数。
## 注册适配器
```typescript
ctx.llm.registerAdapter(['model-name-1', 'model-name-2'], adapter)
```
第一个参数是该适配器支持的模型名列表。当用户在 `cordis.yml` 中配置 `model: model-name-1` 时,框架会路由到这个适配器。
## 在 cordis.yml 中使用
```yaml
- id: my-llm
name: './src/my-llm-adapter.ts'
config:
apiKey: !!js process.env.MY_API_KEY
models:
- my-model-v1
- my-model-v2
- id: stdio-agent
name: '@deepseek-ai/dsh-stdio-agent'
config:
model: my-model-v1 # 引用上面注册的模型名
```
## 实战参考
仓库中有两个完整实现可供参考:
- `packages/llm/llm-deepseek/` — DeepSeek API 适配器OpenAI 兼容格式)
- `packages/llm/llm-pi-ai/` — Pi AI 适配器(不同的 API 格式)
- `examples/echo-agent/src/mock-llm.ts` — 最简 mock 适配器(教学用)
mock 适配器是学习 StreamChunk 协议的最佳起点——它用纯本地逻辑演示了完整的 chunk 序列。
## 错误处理
适配器中的异常会被 agent-loop 捕获并转化为 `LlmError`,告知上层。不需要在 `stream()` 内部做错误恢复——让异常冒泡即可。
```typescript
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
const response = await fetch(this.endpoint, { /* ... */ })
if (!response.ok) {
throw new Error(`API error: ${response.status}`)
}
// ... 正常流式处理
}
```

View File

@@ -0,0 +1,57 @@
# 配置文件
Harness 使用 `cordis.yml` 描述 Agent 加载哪些插件以及每个插件的参数。配置文件负责组合能力;每个包真正支持的字段和默认值由源码生成的配置目录负责记录,避免两份手写表格逐渐不一致。
## 从真实配置开始
仓库中的示例就是可以运行的配置,也是新项目最可靠的起点:
- [echo-agent](../../../../examples/echo-agent/cordis.yml) 使用本地 mock 模型,不需要 API key。
- [coding-agent](../../../../examples/coding-agent/cordis.yml) 组合 DeepSeek 模型、Bash、文件系统、压缩、子代理和工作流。
- [acp-agent](../../../../examples/acp-agent/cordis.yml) 通过 ACP 接入编辑器客户端。
最小配置由一组插件条目组成:
```yaml
- id: llm-deepseek
name: '@deepseek-ai/dsh-llm-deepseek'
config:
apiKey: !!js process.env.DEEPSEEK_API_KEY
models:
- deepseek-v4-flash
- id: stdio-agent
name: '@deepseek-ai/dsh-stdio-agent'
config:
model: deepseek-v4-flash
```
## 插件条目
`name` 指定 npm 包或相对于 `cordis.yml` 的本地模块,`id` 为插件实例提供稳定标识,`config` 传入插件自己的配置。需要临时跳过某个条目时可设置 `disabled: true`
```yaml
- id: local-tool
name: './src/my-tool.ts'
disabled: false
config:
toolName: my_tool
```
插件按文件中的顺序加载。依赖其他服务的插件应该排在提供这些服务的应用或能力插件之后;引用不存在的模型、工具或插件会尽早报错,而不是被静默忽略。
## JavaScript 值和环境变量
Cordis loader 使用 `!!js` 标签读取运行时表达式。API key 等凭据应放在仓库根目录、已被 Git 忽略的 `.env` 中,不能提交到配置文件。
```yaml
config:
apiKey: !!js process.env.DEEPSEEK_API_KEY
cwd: !!js process.cwd()
```
标签是 `!!js`,不是 `!js`
## 精确配置参考
每个插件当前支持的字段、类型和默认值见自动生成的[插件配置目录](../../../config-catalog.md)。理解插件如何组合可继续阅读[架构说明](../../../architecture.md)和[能力接口](../../../capability-seams.md);要创建自己的配置,优先复制并修改[示例目录说明](../../../../examples/README.md)中最接近的例子。

View File

@@ -0,0 +1,47 @@
# 介绍
DeepSeek Harness 是一个**插件化的 Agent 开发框架**,基于 [Cordis](https://github.com/cordiverse/cordis) 微内核构建。它的核心理念是:**一切皆插件**。
## 它是什么
Harness 将一个 AI Agent智能体 所需要的所有能力——LLM 调用、工具执行、会话管理、子任务分配——全部构建为可组合的插件。你通过一个 `cordis.yml` 配置文件来声明加载哪些插件、使用什么参数,就能组装出一个完整的 Agent。
```yaml
# 选择 LLM 后端
- name: '@deepseek-ai/dsh-llm-deepseek'
config:
apiKey: !!js process.env.DEEPSEEK_API_KEY
# 选择应用模板
- name: '@deepseek-ai/dsh-stdio-agent'
config:
model: deepseek-v4-flash
```
## 适合谁
### 应用使用者
如果你只是想用一个现成的 Agent 应用(如编程助手、对话代理),你需要的全部操作就是:
1. 复制一个 example 模板
2. 填写 API key
3. 运行
不需要写任何代码。详见 [快速开始](./quickstart.md)。
### 插件开发者
如果你想为 Agent 添加新能力——一个自定义 tool、一个新的 LLM 适配器、一个新的执行后端——你需要编写一个插件。Harness 提供了清晰的扩展接口和类型安全的开发体验。详见 [开发](../develop/basic/)。
## 核心特性
- **只需要配置** — `cordis.yml` 决定能力集合,换模型、加工具只需改一行
- **随时替换 (HMR)** — 开发时修改插件代码,无需重启进程
## 技术栈
- **运行时**: Node.js ^22.19 或 >= 24
- **语言**: TypeScript (ESM)
- **框架**: Cordis
- **包管理**: pnpm workspaces仓库固定使用 pnpm 11

View File

@@ -0,0 +1,97 @@
# 快速开始
本指南带你在 5 分钟内跑起一个 Agent。
## 环境准备
- [Node.js](https://nodejs.org/) ^22.19 或 >= 24
- [pnpm](https://pnpm.io/) 11建议通过 Corepack 使用仓库固定的版本)
```sh
# 确认版本
node -v # v22.19.x或 v24.x 及更高版本
corepack enable
pnpm -v # 11.x
```
## 第一步:运行 echo-agent
echo-agent 不需要 API key装好依赖就能跑。
```sh
# 克隆仓库
git clone https://github.com/deepseek-harness/deepseek-harness.git
cd deepseek-harness
# 安装依赖
pnpm install
# 启动 echo-agent
pnpm run demo:echo
```
启动后你会看到:
```
echo-agent ready. Type a message ("echo <text>" triggers the tool).
>
```
试着输入:
```
> echo hello world
```
你会看到模型发起了一次 tool call工具调用echo 工具将文本转为大写并返回:
```
[tool call] echo({"text":"hello world"})
[tool result] ECHO: HELLO WORLD
```
恭喜!环境没问题。
## 第二步:使用真实模型调用
接下来接入真实的 DeepSeek 模型,跑一个完整的命令行 Agent。
### 获取 API Key
前往 [DeepSeek Platform](https://platform.deepseek.com/) 获取你的 API key。
### 配置环境变量
在仓库根目录创建 `.env` 文件(已被 gitignore
```sh
DEEPSEEK_API_KEY=sk-your-key-here
```
### 启动 coding-agent
```sh
pnpm run demo:repl
```
```
agent REPL ready. Give it a coding task.
>
```
这就是一个完整的编程助手,它能读写文件、跑命令、拆分子任务。
试着给它一个任务:
```
> 在当前目录创建一个 hello.js内容是打印 "Hello from Harness!",然后运行它
```
## 回头看
echo-agent 和 coding-agent 用的是同一个应用框架(`@deepseek-ai/dsh-stdio-agent`),区别只在 `cordis.yml`——换了哪些插件、填了什么配置。你以后定制自己的 Agent 也是同样的方式。
## 下一步
- [配置文件](./config.md) — 了解 `cordis.yml` 的完整语法
- [开发插件](../develop/basic/) — 编写你自己的 tool 或后端

21
docs/user/zh-CN/index.md Normal file
View File

@@ -0,0 +1,21 @@
---
layout: home
hero:
name: DeepSeek Harness
text: 插件化 Agent 开发框架
tagline: 基于 Cordis 微内核,一切皆插件
actions:
- theme: brand
text: 快速开始
link: /guide/quickstart
- theme: alt
text: 开发插件
link: /develop/basic/
features:
- title: 插件化架构
details: 基于 Cordis 效果系统,所有能力通过插件注册,加载即生效、卸载即还原。
- title: 配置即组合
details: 一个 cordis.yml 决定整个 Agent 的能力组合——换模型、加工具,只需改一行配置。
- title: 开箱即用
details: 内置 LLM 调用、文件读写、Bash 执行、子代理委派等完整工具链,复制模板即可运行。
---