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
+4 -2
View File
@@ -7,6 +7,8 @@ description: Use when publishing, updating, moving, or removing DeepSeek Harness
Keep repository Markdown as the only editable content source. Treat the website as a tested projection: [website/docs.ts](../../../website/docs.ts) selects public pages, [scripts/project-doc-site.ts](../../../scripts/project-doc-site.ts) rewrites them into the disposable `website/.generated/` tree, and VitePress builds that tree.
Repository translations follow the sibling pairing contract: English `foo.md`, Chinese `foo.zh.md`, and `foo.i18n.yaml` live together. Never create `zh-CN/` or other locale directories for website content. The site route trees are independent of that source layout: `foo.zh.md` projects to the root route and `foo.md` projects to the matching `/en/` route.
## Read the owning contracts
- Read [docs/AGENTS.md](../../../docs/AGENTS.md) and use [dsh-doc-standards](../dsh-doc-standards/SKILL.md) when deciding where content belongs or changing product documentation prose.
@@ -28,7 +30,7 @@ Never edit or commit `website/.generated/`, `website/.cache/`, or `website/.dist
Set every `DocsPage` field deliberately:
- `source`: repository-relative canonical Markdown path.
- `source`: repository-relative canonical Markdown path. For a complete bilingual pair, add the English `.md` path through `pairedPages()`; it derives the sibling `.zh.md`, the content locales, and counterpart aliases.
- `route`: public VitePress path including the `.md` suffix.
- `label`: sidebar label, not necessarily the document H1.
- `sidebar`: reuse `zh-guide`, `zh-develop`, or `en-docs` unless the information architecture genuinely needs another collection.
@@ -36,7 +38,7 @@ Set every `DocsPage` field deliberately:
- `order`: stable order within the section.
- `sourceAliases`: optional additional repository paths that should resolve to this page when links are projected. It does not create another public route.
Keep the manifest an explicit public allowlist. Do not publish RFCs, postmortems, testing guides, `AGENTS.md`, or maintainer workflows merely because they exist under `docs/`; add internal material only when the user explicitly changes the publication boundary.
Use `mirroredPages()` only for a source that intentionally falls back to the same available language in both route trees. Convert that entry to `pairedPages()` when its counterpart is added. Keep the manifest an explicit public allowlist. Do not publish RFCs, postmortems, testing guides, `AGENTS.md`, or maintainer workflows merely because they exist under `docs/`; add internal material only when the user explicitly changes the publication boundary.
## Preserve link behavior
+1 -1
View File
@@ -15,7 +15,7 @@ Each fact has one home: the tier whose job it is. Elsewhere, link to that home;
| [rfc/](rfc/README.md) | Decision records: the why, what-was-given-up, and concise verification contract; `implemented/` RFCs describe shipped reality in present tense | Migration plans, acceptance-task checklists, fixture walkthroughs, 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 |
| [user/](user/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, and [Model Experience](cookbook/adding-a-package.md#4-write-the-package-readme) | 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 |
+6
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
config.md: 5c4e712e2ae452d30fde23bd2481f0c5260ee526
config.zh.md: 23c97e18a119a92fe7ae883621cb9340ef54bf80
+111
View File
@@ -0,0 +1,111 @@
# Plugin configuration
English | [中文](config.zh.md)
Accept configuration supplied through `cordis.yml`.
## Define the Config type
Export a `Config` type and a same-named Schemastery schema. Put defaults directly on the schema fields:
```ts
import type { Context } from 'cordis'
import Schema from 'schemastery'
export const name = 'my-plugin'
export interface Config {
greeting: string
maxRetries: number
verbose?: boolean
}
export const Config: Schema<Config> = Schema.object({
greeting: Schema.string().default('Hello'),
maxRetries: Schema.number().default(3),
verbose: Schema.boolean().default(false),
})
export function apply(ctx: Context, config: Config) {
console.log(config.greeting) // User value or schema default.
}
```
Configure it in `cordis.yml`:
```yaml
- name: './src/my-plugin.ts'
config:
greeting: 'Hi there'
maxRetries: 5
```
When loading the plugin, Cordis uses the exported schema to validate configuration and fill defaults. Do not export a plain object as `Config`; it does not implement the Standard Schema interface required by Cordis.
## Schema validation
Use Schemastery to express stricter validation:
```ts
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 is validated and type-safe.
}
```
The schema runs while the plugin loads. Invalid configuration fails the load with an actionable error.
## Design principles
### Do not hardcode tunable values
Harness requires **anything that two deployments may want to set differently to be a configuration field**.
```ts
// Wrong: hardcoded timeout.
const TIMEOUT = 30000
// Correct: configurable.
export interface Config {
timeoutMs: number // Defaults to 30000.
}
```
The test is whether `cordis.yml` can change the value without a code edit.
### Fail loudly on invalid configuration
If configuration refers to a missing model or another nonexistent resource, fail early instead of silently skipping it:
```ts ignore-check
export function apply(ctx: Context, config: Config) {
if (!ctx.llm.models().includes(config.model)) {
throw new Error(`Model "${config.model}" is not registered by any LLM adapter`)
}
}
```
## Work with HMR
A configuration edit hot-replaces the plugin: the framework unloads the old instance and loads a new one. Because registrations are effects and clean themselves up, replacement does not retain the old instance's registrations.
## Next steps
- [Plugins and lifecycle](../framework/) — understand the full plugin lifecycle
- [Services and dependencies](../framework/service.md) — provide a service to other plugins
@@ -1,12 +1,14 @@
# 插件配置
[English](config.md) | 中文
让你的插件接受用户在 `cordis.yml` 中传入的配置。
## 定义 Config 类型
在插件中导出一个 `Config` 类型和同名的 Schemastery schema;默认值直接写在 schema 中:
```typescript
```ts
import type { Context } from 'cordis'
import Schema from 'schemastery'
@@ -25,7 +27,7 @@ export const Config: Schema<Config> = Schema.object({
})
export function apply(ctx: Context, config: Config) {
console.log(config.greeting) // 用户配置或默认值
console.log(config.greeting) // User value or schema default.
}
```
@@ -44,7 +46,7 @@ export function apply(ctx: Context, config: Config) {
对于需要严格校验的场景,使用 Schemastery 定义 schema
```typescript
```ts
import type { Context } from 'cordis'
import Schema from 'schemastery'
@@ -63,7 +65,7 @@ export const Config = Schema.object({
})
export function apply(ctx: Context, config: Config) {
// config 已经过校验,类型安全
// config is validated and type-safe.
}
```
@@ -75,13 +77,13 @@ Schema 在插件加载时执行校验。如果配置不合法,插件会加载
Harness 的约定:**任何两个部署可能想要不同值的东西,都应该是配置字段**。
```typescript
// 错误 — 硬编码超时时间
```ts
// Wrong: hardcoded timeout.
const TIMEOUT = 30000
// 正确 — 可配置
// Correct: configurable.
export interface Config {
timeoutMs: number // 默认 30000
timeoutMs: number // Defaults to 30000.
}
```
@@ -91,7 +93,7 @@ export interface Config {
如果配置引用了不存在的东西(比如一个不存在的模型名),应该尽早报错,而不是静默跳过:
```typescript
```ts ignore-check
export function apply(ctx: Context, config: Config) {
if (!ctx.llm.models().includes(config.model)) {
throw new Error(`Model "${config.model}" is not registered by any LLM adapter`)
+6
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: 5fa46806bc195ad2566fc0a29b45eb1dd7a68179
index.zh.md: a6d238c12841c8c25b00376ee032e5db50fc6b4e
+151
View File
@@ -0,0 +1,151 @@
# Your first plugin
English | [中文](index.zh.md)
This guide creates a minimal Harness plugin and loads it into an agent.
## What is a plugin?
In Harness, a plugin is a TypeScript module that exports an `apply` function. The framework calls `apply` when loading the plugin and passes a `ctx` context object through which the plugin registers capabilities:
```ts
import type { Context } from 'cordis'
export const name = 'my-plugin'
export function apply(ctx: Context) {
// Register capabilities here.
}
```
That is the complete shape.
## Create the plugin file
Create `src/my-plugin.ts` in your project:
```ts
import type { Context } from 'cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) {
// Required dependencies are ready before apply runs.
console.log('[hello-plugin] plugin loaded!')
}
```
## Register it in cordis.yml
Add an entry to `cordis.yml`:
```yaml
- id: hello
name: './src/my-plugin.ts'
```
After startup, the console prints `[hello-plugin] plugin loaded!`.
## Automatic cleanup
Anything registered through `ctx`—event listeners, tools, or timers—is cleaned up when the plugin unloads. You do not need to call removeListener or clearInterval manually.
For a resource that needs explicit cleanup, such as a network connection, use `ctx.effect()` to provide its disposer:
```ts
import type { Context } from 'cordis'
export function apply(ctx: Context) {
ctx.effect(() => {
const timer = setInterval(() => {
console.log('heartbeat')
}, 5000)
// The returned function runs when the plugin unloads.
return () => clearInterval(timer)
})
}
```
## Declare dependencies
If the plugin consumes another service such as `tools` or `llm`, declare it in `inject`:
```ts ignore-check
import type { Context } from 'cordis'
export const name = 'my-tool-plugin'
export const inject = ['tools']
export function apply(ctx: Context) {
// ctx.tools is ready here.
ctx.tools.register(/* ... */)
}
```
The framework waits for every required service before loading the plugin.
## Three plugin forms
In addition to a function module, a plugin can use object or class form.
### Object form
```ts
import type { Context } from 'cordis'
export default {
name: 'my-plugin',
inject: ['tools'],
apply(ctx: Context) {
// ...
},
}
```
### Class form
```ts
import { Service, type Context } from 'cordis'
export default class MyService extends Service {
static inject = ['tools']
constructor(ctx: Context) {
super(ctx, 'myService')
// Perform synchronous initialization in the constructor.
}
}
```
Function form is sufficient in most cases. Use class form when the plugin provides a service to other plugins; see [services and dependencies](../framework/service.md).
## Complete example
`examples/echo-agent/src/echo-tool.ts` is a plugin that registers a tool:
```ts
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()}` }]
},
}))
}
```
## Next steps
- [Build a tool](./tool.md) — learn the tool definition DSL
- [Plugin configuration](./config.md) — accept user configuration
@@ -1,18 +1,20 @@
# 第一个插件
[English](index.md) | 中文
本文带你编写一个最小的 Harness 插件并加载到 Agent 中。
## 插件是什么
在 Harness 中,插件是一个导出 `apply` 函数的 TypeScript 模块。框架在加载时调用 `apply`,传入一个 `ctx`(上下文对象),你通过 `ctx` 注册能力:
```typescript
```ts
import type { Context } from 'cordis'
export const name = 'my-plugin'
export function apply(ctx: Context) {
// 在这里注册能力
// Register capabilities here.
}
```
@@ -22,14 +24,14 @@ export function apply(ctx: Context) {
在你的项目目录下创建 `src/my-plugin.ts`
```typescript
```ts
import type { Context } from 'cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) {
// apply 被调用时,插件的必选依赖已就绪
console.log('[hello-plugin] 插件已加载!')
// Required dependencies are ready before apply runs.
console.log('[hello-plugin] plugin loaded!')
}
```
@@ -42,7 +44,7 @@ export function apply(ctx: Context) {
name: './src/my-plugin.ts'
```
启动后你会在控制台看到 `[hello-plugin] 插件已加载!`
启动后你会在控制台看到 `[hello-plugin] plugin loaded!`
## 自动清理
@@ -50,14 +52,16 @@ export function apply(ctx: Context) {
如果你有需要手动清理的资源(比如一个网络连接),用 `ctx.effect()` 告诉框架怎么清理:
```typescript
```ts
import type { Context } from 'cordis'
export function apply(ctx: Context) {
ctx.effect(() => {
const timer = setInterval(() => {
console.log('heartbeat')
}, 5000)
// 返回的函数会在插件卸载时被调用
// The returned function runs when the plugin unloads.
return () => clearInterval(timer)
})
}
@@ -67,12 +71,14 @@ export function apply(ctx: Context) {
如果你的插件需要使用其他服务(如 `tools``llm`),需要声明 `inject`
```typescript
```ts ignore-check
import type { Context } from 'cordis'
export const name = 'my-tool-plugin'
export const inject = ['tools']
export function apply(ctx: Context) {
// ctx.tools 现在可用
// ctx.tools is ready here.
ctx.tools.register(/* ... */)
}
```
@@ -85,7 +91,9 @@ export function apply(ctx: Context) {
### 对象形式
```typescript
```ts
import type { Context } from 'cordis'
export default {
name: 'my-plugin',
inject: ['tools'],
@@ -97,7 +105,7 @@ export default {
### 类形式
```typescript
```ts
import { Service, type Context } from 'cordis'
export default class MyService extends Service {
@@ -105,7 +113,7 @@ export default class MyService extends Service {
constructor(ctx: Context) {
super(ctx, 'myService')
// 构造函数内完成同步初始化
// Perform synchronous initialization in the constructor.
}
}
```
@@ -116,7 +124,7 @@ export default class MyService extends Service {
参考仓库中的 `examples/echo-agent/src/echo-tool.ts`,这是一个注册 tool 的插件:
```typescript
```ts
import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
+6
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
tool.md: 416733bcb584fa5303a8b3ba5e6e904302e7f992
tool.zh.md: fce9a7d9b973853c8b4fb9ae2c034e749d8da999
+208
View File
@@ -0,0 +1,208 @@
# Build a tool
English | [中文](tool.zh.md)
A tool is a capability the model can call. This guide builds one with `defineTool`.
## Minimal example
```ts
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 is inferred as { name: string }.
return [{ type: 'text', text: `Hello, ${args.name}!` }]
},
}))
}
```
## Parameter definitions
`parameters` uses a compact format that the framework converts to the JSON Schema sent to the model.
### Primitive types
```ts
export const parameters = {
path: { type: 'string', required: true },
limit: { type: 'number' },
recursive: { type: 'boolean' },
}
// Inferred type: { path: string; limit?: number; recursive?: boolean }
```
### Enums
```ts
export const parameters = {
mode: { type: 'string', required: true, enum: ['read', 'write', 'append'] },
}
// Inferred type: { mode: string } (enum values are validated at runtime)
```
### Nested objects
```ts
export const parameters = {
options: {
type: 'object',
properties: {
timeout: { type: 'number' },
retries: { type: 'number' },
},
},
}
// Inferred type: { options?: { timeout?: number; retries?: number } }
```
### Arrays
```ts
export const parameters = {
tags: {
type: 'array',
items: { type: 'string' },
},
}
// Inferred type: { tags?: string[] }
```
### Property fields
| Field | Type | Meaning |
|------|------|------|
| `type` | `'string' \| 'number' \| 'boolean' \| 'object' \| 'array'` | Value type |
| `required` | `true` | Marks the property required and affects inference |
| `description` | `string` | Description sent to the model |
| `enum` | `string[]` | Allowed string values |
| `properties` | `SchemaSpec` | Nested properties for an object |
| `items` | `SchemaProp` | Element schema for an array |
## The execute function
`execute` receives validated, inferred `args` and an `exec` execution context:
```ts
import { defineTool } from '@deepseek-ai/dsh-tools'
export const tool = defineTool({
name: 'example',
description: 'Return an example result.',
parameters: {},
async execute(args, exec) {
// args: inferred from parameters
// exec: ToolExecution context
// Return a ContentBlock array.
void args
void exec
return [{ type: 'text', text: 'result here' }]
},
})
```
### Return value
`execute` returns a `ContentBlock[]` that becomes the tool result visible to the model:
```ts ignore-check
// Text result
return [{ type: 'text', text: 'file content here...' }]
// Multiple blocks
return [
{ type: 'text', text: 'Found 3 matches:' },
{ type: 'text', text: matchResults.join('\n') },
]
```
### Argument validation
Before calling `execute`, `defineTool` validates model-generated arguments. Invalid input raises `ToolArgsError`; the framework turns it into an `isError` result so the model can correct its call.
Do not repeat type validation inside `execute`.
## Presentation
A tool can define UI presentation methods for terminal and ACP clients:
```ts ignore-check
defineTool({
name: 'bash',
// ...
presentCall(args) {
return {
card: 'terminal',
title: args.command,
}
},
presentResult(args, result) {
return {
card: 'terminal',
output: result.content.map(b => b.type === 'text' ? b.text : '').join(''),
}
},
})
```
`presentCall` and `presentResult` are **pure functions**. Streaming UI and session replay may call them more than once.
## Registration and unloading
`ctx.tools.register()` returns a disposer, but a registration made through `ctx` is already tracked by the framework. Unloading the plugin removes the tool automatically, so the plugin does not call the disposer itself.
```ts ignore-check
// This is sufficient:
ctx.tools.register(defineTool({ /* ... */ }))
// No saved disposer or extra cleanup registration is needed.
```
## Complete example
This tool counts files in a directory:
```ts
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.` }]
},
}))
}
```
## Next steps
- [Plugin configuration](./config.md) — make the tool configurable
- [Capability layering](../practice/) — understand the interface/implementation/consumer pattern
@@ -1,10 +1,12 @@
# 开发一个 Tool
[English](tool.md) | 中文
Tool 是模型可以调用的能力。本文介绍如何用 `defineTool` 编写一个 tool。
## 最小示例
```typescript
```ts
import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
@@ -19,7 +21,7 @@ export function apply(ctx: Context) {
name: { type: 'string', required: true, description: 'The name to greet' },
},
async execute(args) {
// args 自动推导为 { name: string }
// args is inferred as { name: string }.
return [{ type: 'text', text: `Hello, ${args.name}!` }]
},
}))
@@ -32,28 +34,28 @@ export function apply(ctx: Context) {
### 基本类型
```typescript
parameters: {
```ts
export const parameters = {
path: { type: 'string', required: true },
limit: { type: 'number' },
recursive: { type: 'boolean' },
}
// 推导类型: { path: string; limit?: number; recursive?: boolean }
// Inferred type: { path: string; limit?: number; recursive?: boolean }
```
### 枚举
```typescript
parameters: {
```ts
export const parameters = {
mode: { type: 'string', required: true, enum: ['read', 'write', 'append'] },
}
// 推导类型: { mode: string } (运行时校验 enum 值)
// Inferred type: { mode: string } (enum values are validated at runtime)
```
### 嵌套对象
```typescript
parameters: {
```ts
export const parameters = {
options: {
type: 'object',
properties: {
@@ -62,19 +64,19 @@ parameters: {
},
},
}
// 推导类型: { options?: { timeout?: number; retries?: number } }
// Inferred type: { options?: { timeout?: number; retries?: number } }
```
### 数组
```typescript
parameters: {
```ts
export const parameters = {
tags: {
type: 'array',
items: { type: 'string' },
},
}
// 推导类型: { tags?: string[] }
// Inferred type: { tags?: string[] }
```
### 每个属性的字段
@@ -92,25 +94,34 @@ parameters: {
`execute` 接收经过校验的 `args`(类型自动推导)和一个 `exec` 上下文对象:
```typescript
async execute(args, exec) {
// args: 根据 parameters 自动推导的类型
// exec: ToolExecution 对象,提供执行上下文
```ts
import { defineTool } from '@deepseek-ai/dsh-tools'
// 返回 ContentBlock 数组
return [{ type: 'text', text: 'result here' }]
}
export const tool = defineTool({
name: 'example',
description: 'Return an example result.',
parameters: {},
async execute(args, exec) {
// args: inferred from parameters
// exec: ToolExecution context
// Return a ContentBlock array.
void args
void exec
return [{ type: 'text', text: 'result here' }]
},
})
```
### 返回值
`execute` 必须返回一个 `ContentBlock[]`,告诉模型 tool 的执行结果:
```typescript
// 文本结果
```ts ignore-check
// Text result
return [{ type: 'text', text: 'file content here...' }]
// 多个 block
// Multiple blocks
return [
{ type: 'text', text: 'Found 3 matches:' },
{ type: 'text', text: matchResults.join('\n') },
@@ -127,7 +138,7 @@ return [
Tool 可以定义 UI 渲染方法,用于在终端或 ACP 客户端中展示 tool call 和 result
```typescript
```ts ignore-check
defineTool({
name: 'bash',
// ...
@@ -152,18 +163,18 @@ defineTool({
`ctx.tools.register()` 返回值就是 disposer。但由于你在 `ctx` 上调用,框架已经自动追踪了这个注册——插件卸载时会自动移除 tool。你不需要手动调用 disposer。
```typescript
// 这样就够了:
```ts ignore-check
// This is sufficient:
ctx.tools.register(defineTool({ /* ... */ }))
// 不需要额外保存 disposer 或注册清理逻辑
// No saved disposer or extra cleanup registration is needed.
```
## 完整实战示例
一个文件计数 tool
```typescript
```ts
import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { readdir } from 'node:fs/promises'
@@ -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
@@ -1,20 +1,22 @@
# 事件系统
[English](events.md) | 中文
事件是 Cordis 插件间通信的核心机制。Harness 大量使用事件来实现松耦合的扩展点。
## 基本用法
### 监听事件
```typescript
```ts ignore-check
ctx.on('event-name', (payload) => {
// 处理事件
// Handle the event.
})
```
### 触发事件
```typescript
```ts ignore-check
ctx.emit('event-name', payload)
```
@@ -26,11 +28,11 @@ Cordis 提供多种事件触发模式,适用于不同场景:
所有监听器同步执行,不关心返回值:
```typescript
// 触发
```ts ignore-check
// Emit
ctx.emit('my-plugin/ready', { id: 'worker-1' })
// 监听
// Listen
ctx.on('my-plugin/ready', ({ id }) => {
console.log(`${id} is ready`)
})
@@ -40,14 +42,14 @@ ctx.on('my-plugin/ready', ({ id }) => {
依次调用监听器,第一个返回非 `undefined` 值的结果作为最终值:
```typescript
// 触发
```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'
// 返回 undefined 继续传递给下一个监听器
// Return undefined to continue to the next listener.
})
```
@@ -55,7 +57,7 @@ ctx.on('some-check', (input) => {
监听器按注册顺序依次执行,并等待异步结果;第一个返回非空值的监听器会终止后续执行:
```typescript
```ts ignore-check
await ctx.serial('setup-phase', context)
```
@@ -63,11 +65,11 @@ await ctx.serial('setup-phase', context)
每个监听器可以包装下游返回值,形成处理链。**必须调用 `next()` 传递给下游**,不调用即为否决:
```typescript
// 触发
```ts ignore-check
// Dispatch
const output = await ctx.waterfall('my-plugin/transform', input, async () => input)
// 监听(必须调用 next
// Listen: next() is mandatory.
ctx.on('my-plugin/transform', async (_input, next) => {
const downstream = await next()
return downstream.trim()
@@ -82,7 +84,9 @@ Waterfall 监听器**必须调用 `next()`**。不调用 `next` 等于否决整
Harness 使用 TypeScript 声明合并来为事件提供类型安全:
```typescript
```ts
import 'cordis'
declare module 'cordis' {
interface Events {
'my-plugin/ready': (payload: { id: string }) => void
@@ -91,13 +95,13 @@ declare module 'cordis' {
}
}
// 现在 ctx.on('my-plugin/ready', ...) ctx.emit('my-plugin/ready', ...)
// 都有正确的类型推导
// 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)。
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`
@@ -105,9 +109,9 @@ Harness 的 Cordis 事件遵循 `namespace/action` 命名,例如 `agent/pre-st
通过 `ctx.on()` 注册的监听器会在插件卸载时自动移除:
```typescript
```ts ignore-check
export function apply(ctx: Context) {
// 这个监听器在插件 dispose 时自动清理
// This listener is removed when the plugin disposes.
ctx.on('tools/result', handler)
}
```
@@ -116,8 +120,9 @@ export function apply(ctx: Context) {
一个记录所有 tool 调用的简单插件:
```typescript
```ts
import type { Context } from 'cordis'
import '@deepseek-ai/dsh-tools'
export const name = 'tool-logger'
@@ -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
@@ -1,5 +1,7 @@
# 插件与生命周期
[English](index.md) | 中文
深入了解 Cordis 插件模型和生命周期状态机。
## Fiber 状态机
@@ -25,11 +27,11 @@ ACTIVE → UNLOADING → DISPOSED
声明了 `inject` 的插件不会立即加载,而是等待依赖的服务就绪:
```typescript
```ts ignore-check
export const inject = ['tools', 'llm']
export function apply(ctx: Context) {
// 到这里时,ctx.tools ctx.llm 一定存在
// ctx.tools and ctx.llm are ready here.
}
```
@@ -39,12 +41,12 @@ export function apply(ctx: Context) {
通过 `ctx` 做的任何注册,在插件卸载时都会自动撤销:
```typescript
```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()
@@ -58,18 +60,18 @@ export function apply(ctx: Context) {
- `ctx.llm.registerAdapter(names, adapter)` — LLM 适配器注册
- `ctx.effect(() => cleanup)` — 自定义资源
插件卸载时,这些注册按倒序逐个撤销
插件卸载时,处置器按注册顺序的反向发起,但多个异步处置器会并发执行,不保证逐个完成。存在顺序依赖的清理步骤必须放进同一个 `ctx.effect()` 返回的处置器中,由该处置器负责串行等待
## 嵌套上下文
`ctx.plugin()` 创建子 Fiber,它继承父上下文但有独立的生命周期:
```typescript
```ts ignore-check
export function apply(ctx: Context) {
// 注册一个子插件
// Register a child plugin.
ctx.plugin(childPlugin)
// 子插件有自己的 Fiber,父卸载时子也卸载
// The child has its own Fiber and unloads with its parent.
}
```
@@ -77,10 +79,10 @@ export function apply(ctx: Context) {
当你需要提前终止一个插件实例:
```typescript
```ts ignore-check
const fiber = ctx.plugin(myPlugin)
// 之后可以手动 dispose
// Dispose it manually later.
fiber.dispose()
```
@@ -101,7 +103,7 @@ fiber.dispose()
## 实战:理解生命周期
```typescript
```ts ignore-check
export function apply(ctx: Context) {
console.log('plugin loading')
@@ -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
@@ -1,15 +1,17 @@
# 服务与依赖
[English](service.md) | 中文
服务 (Service) 是插件对外暴露能力的方式。依赖 (inject) 是插件声明自己需要哪些服务。
## 什么是服务
在 Harness 中,`tools``llm``agents` 都是服务。服务是挂载在 `ctx` 上的命名能力:
```typescript
ctx.tools // ToolRegistry 服务
ctx.llm // LLM 服务
ctx.agents // Agent 服务
```ts ignore-check
ctx.tools // ToolRegistry service
ctx.llm // LLM service
ctx.agents // Agent service
```
任何插件都可以提供一个新服务,供其他插件使用。
@@ -18,11 +20,11 @@ ctx.agents // Agent 服务
声明 `inject` 来使用已有服务:
```typescript
```ts ignore-check
export const inject = ['tools']
export function apply(ctx: Context) {
// ctx.tools 在这里一定存在且就绪
// ctx.tools exists and is ready here.
ctx.tools.register(/* ... */)
}
```
@@ -33,17 +35,17 @@ export function apply(ctx: Context) {
### 使用 Service 基类
```typescript
```ts
import { Service, type Context } from 'cordis'
export default class MetricsService extends Service {
static inject = ['llm'] // 本服务也可以依赖其他服务
static inject = ['llm'] // A service may depend on other services.
constructor(ctx: Context) {
super(ctx, 'metrics') // 'metrics' 是服务名
super(ctx, 'metrics') // 'metrics' is the service name.
}
// 服务的公开方法
// Public service method.
record(event: string, value: number) {
// ...
}
@@ -52,7 +54,7 @@ export default class MetricsService extends Service {
加载这个插件后,其他插件就可以通过 `ctx.metrics` 访问它:
```typescript
```ts ignore-check
export const inject = ['metrics']
export function apply(ctx: Context) {
@@ -64,7 +66,7 @@ export function apply(ctx: Context) {
使用 TypeScript 声明合并让 `ctx.metrics` 有正确类型:
```typescript
```ts
import { Service, type Context } from 'cordis'
declare module 'cordis' {
@@ -86,11 +88,11 @@ export default class MetricsService extends Service {
### 必选依赖 vs 可选依赖
```typescript
// 必选:服务不存在时,插件不会加载
```ts ignore-check
// Required: the plugin does not load while the service is absent.
export const inject = ['tools']
// 可选:不写入 inject,使用时通过 ctx.get() 查询
// 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)
@@ -138,7 +140,7 @@ export function apply(ctx: Context) {
## Harness 内置服务
服务名、公开方法和源码位置由仓库自动生成,见[服务目录](../../../../cordis-catalog/services.md)。开发插件时应以该目录和服务接口的 TypeScript 类型为准,不要复制一份静态清单。
服务名、公开方法和源码位置由仓库自动生成,见[服务目录](../../../cordis-catalog/services.md)。开发插件时应以该目录和服务接口的 TypeScript 类型为准,不要复制一份静态清单。
## 下一步
@@ -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: 0261b49b071167f7c2a33f78bbc1959cc6f1879f
index.zh.md: 5819344430fcbde31bf825e9815120983e44e3f6
+158
View File
@@ -0,0 +1,158 @@
# Three-layer capability design
English | [中文](index.zh.md)
When a capability is general enough to need replaceable implementations, such as Bash execution, Harness splits it into three packages: an **interface**, an **implementation**, and a **consumer**. Each layer can evolve or be replaced independently.
## Bash example
The Bash execution capability consists of:
- **Interface** (`dsh-bash`) — defines Bash request and result shapes
- **Implementation** (`dsh-bash-local`) — executes commands on the local machine
- **Consumer** (`dsh-tool-bash`) — exposes the capability as a model-callable tool
```
┌─────────────┐ ┌──────────────────┐ ┌──────────────┐
│ dsh-bash │────▶│ dsh-bash-local │ │ dsh-tool-bash│
│ (interface) │ │ (implementation) │ │(consumer/tool)│
└─────────────┘ └──────────────────┘ └──────────────┘
▲ │
└────────────────────────────────────────────┘
inject: ['bash']
```
## Benefits of the split
### Replace implementations
One interface can have multiple implementations selected through `cordis.yml`:
```yaml
# Local execution
- name: '@deepseek-ai/dsh-bash-local'
# Or a future remote sandbox implementation
# - name: '@deepseek-ai/dsh-bash-remote'
# config:
# endpoint: 'https://sandbox.example.com'
```
The interface and tool remain unchanged while the implementation changes.
### Evolve independently
- The interface changes rarely after its contract stabilizes.
- Implementations can improve performance and security independently.
- Consumers can change how they present the capability to the model.
### Decouple dependencies
- The implementation depends on the interface.
- The consumer depends on the interface.
- The implementation and consumer **do not depend on each other**.
## Built-in three-layer capabilities
| Capability | Interface | Implementation | Consumer |
|------|-------------|------|---------------|
| Bash | `dsh-bash` | `dsh-bash-local` | `dsh-tool-bash` |
| Filesystem | `dsh-fs` | `dsh-fs-local` + `dsh-fs-policy` | `dsh-tool-fs` |
| Web | `dsh-web` | `dsh-web-fetch-local` / `dsh-web-search-*` | `dsh-tool-web` |
| Subagent | `dsh-subagent` | `dsh-subagent-spawn` / `dsh-subagent-fork` | `dsh-tool-subagent` |
| Compaction | `dsh-compact` | `dsh-compact-basic` | The implementation consumes agent-loop extension events |
## Develop a three-layer capability
### Step 1: define the interface
```ts ignore-check
// 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')
}
/** Execute the capability. */
abstract execute(request: MyCapRequest): Promise<MyCapResult>
}
export interface MyCapRequest {
input: string
}
export interface MyCapResult {
output: string
}
```
### Step 2: write an implementation
```ts ignore-check
// 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> {
// Concrete implementation.
return { output: request.input.toUpperCase() }
}
}
export const name = 'my-cap-local'
export function apply(ctx: Context) {
ctx.plugin(MyCapLocal)
}
```
### Step 3: write a consumer
```ts ignore-check
// 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 }]
},
}))
}
```
### Compose them in cordis.yml
```yaml
- name: '@deepseek-ai/dsh-my-cap-local'
- name: '@deepseek-ai/dsh-tool-my-cap'
```
## Design points
- **Do not split preemptively** — use three packages only when the capability needs replaceable implementations. A simple tool plugin does not.
- **The interface owns Request/Result types** — implementations and consumers depend only on the interface package.
- **Explicit > implicit** — resolve defaults in an explicit `resolve(request): Spec` step rather than hiding `?? default` expressions inside `run()`.
## Next steps
- [LLM adapter](./llm-adapter.md) — implement an LLM backend, a common capability interface extension
@@ -1,5 +1,7 @@
# 能力的三层拆分
[English](index.md) | 中文
当一个能力(插件)足够通用(比如"执行 bash 命令"),Harness 会把它拆成三个包:**接口**、**实现**、**消费者**。这样可以独立替换其中任何一层。
## 以 Bash 为例
@@ -13,7 +15,7 @@
```
┌─────────────┐ ┌──────────────────┐ ┌──────────────┐
│ dsh-bash │────▶│ dsh-bash-local │ │ dsh-tool-bash│
(接口) │ │ (实现) │ │ (消费者/tool)│
(interface) │ │ (implementation) │ │(consumer/tool)│
└─────────────┘ └──────────────────┘ └──────────────┘
▲ │
└────────────────────────────────────────────┘
@@ -27,10 +29,10 @@
同一个接口可以有多种实现。用户通过 `cordis.yml` 选择:
```yaml
# 本地执行
# Local execution
- name: '@deepseek-ai/dsh-bash-local'
# 或:远程沙箱执行(未来)
# Or a future remote sandbox implementation
# - name: '@deepseek-ai/dsh-bash-remote'
# config:
# endpoint: 'https://sandbox.example.com'
@@ -64,7 +66,7 @@
### 第一步:定义接口
```typescript
```ts ignore-check
// packages/my-cap/my-cap/src/index.ts
import { Service, type Context } from 'cordis'
@@ -79,7 +81,7 @@ export abstract class MyCapService extends Service {
super(ctx, 'myCap')
}
/** 执行能力的核心方法 */
/** Execute the capability. */
abstract execute(request: MyCapRequest): Promise<MyCapResult>
}
@@ -94,14 +96,14 @@ export interface MyCapResult {
### 第二步:编写实现
```typescript
```ts ignore-check
// 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> {
// 具体实现
// Concrete implementation.
return { output: request.input.toUpperCase() }
}
}
@@ -115,7 +117,7 @@ export function apply(ctx: Context) {
### 第三步:编写消费者 (tool)
```typescript
```ts ignore-check
// packages/my-cap/tool-my-cap/src/index.ts
import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
@@ -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
llm-adapter.md: 18e05ab79f7daf9f86fe1eb27bdd4440fb9107bc
llm-adapter.zh.md: f3c1ac70f7b4c11fb9f6dcb247be342fb358bd39
+185
View File
@@ -0,0 +1,185 @@
# LLM adapters
English | [中文](llm-adapter.zh.md)
This guide connects a new LLM provider to Harness.
## Overview
An LLM adapter extends `LlmAdapter` and implements `stream()`, translating Harness's provider-neutral request into a provider API call and translating the response back into Harness chunks.
## Minimal implementation
```ts
import type { Context } from 'cordis'
import Schema from 'schemastery'
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. Convert options.messages to the provider format.
// 2. Call the streaming API.
// 3. Convert the response into StreamChunk values.
}
}
export interface Config {
apiKey: string
models: string[]
}
export const Config: Schema<Config> = Schema.object({
apiKey: Schema.string().required(),
models: Schema.array(Schema.string()).required(),
})
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 protocol
`stream()` yields chunks using this protocol:
```ts
import { CallId, type StreamChunk } from '@deepseek-ai/dsh-llm'
async function* exampleChunks(): AsyncIterable<StreamChunk> {
// 1. Start each content block with block-start.
yield { type: 'block-start', index: 0, blockType: 'text' }
// 2. Stream text through text-delta.
yield { type: 'text-delta', index: 0, text: 'Hello' }
yield { type: 'text-delta', index: 0, text: ' world' }
// 3. End each content block with block-end and the complete block.
yield {
type: 'block-end',
index: 0,
block: { type: 'text', text: 'Hello world' },
}
// 4. Tool-call block.
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 usage.
yield { type: 'usage', usage: { inputTokens: 100, outputTokens: 50 } }
// 6. Finish reason.
yield { type: 'finish', reason: { kind: 'stop' } }
// Alternatively, { kind: 'tool-calls' } requests tool execution.
}
```
### Key rules
- Every `block-start` has a matching `block-end`.
- `index` increases from 0 and identifies content-block order.
- A `tool-call-delta` carries raw JSON text in `argumentsDelta`, either all at once or over multiple chunks.
- `finish` is the final chunk.
- Emit `usage` before `finish`.
## GenerateOptions
`stream()` receives the exported `GenerateOptions` type. It includes the model, conversation history, system prompt, tool schemas, generation parameters, stop sequences, and abort signal; treat the TypeScript type exported by `@deepseek-ai/dsh-llm` as authoritative. Map supported fields to the provider API. If the provider cannot honor a field, throw `LlmError` with a stable code instead of silently dropping it.
## Register an adapter
```ts ignore-check
ctx.llm.registerAdapter(['model-name-1', 'model-name-2'], adapter)
```
The first argument lists the model names handled by the adapter. If `cordis.yml` selects `model: model-name-1`, the service routes that request to this adapter.
## Use it from 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 # References the model registered above.
```
## Reference implementations
The repository contains complete implementations:
- `packages/llm/llm-deepseek/` — DeepSeek API adapter using the OpenAI-compatible format
- `packages/llm/llm-pi-ai/` — Pi AI adapter using a different API format
- `examples/echo-agent/src/mock-llm.ts` — minimal local teaching adapter
Start with the mock adapter to study a complete chunk sequence without network behavior.
## Error handling
Adapters throw transport and protocol failures as `LlmError` values with stable codes. The agent loop preserves the error and code for diagnostics and policy; it does not convert an ordinary `Error` automatically. Every provider HTTP request must also merge `attributionHeaders()` and forward `options.signal`.
```ts
import {
attributionHeaders,
LlmAdapter,
LlmError,
type GenerateOptions,
type StreamChunk,
} from '@deepseek-ai/dsh-llm'
class HttpAdapter extends LlmAdapter {
constructor(private readonly endpoint: string) {
super()
}
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
const response = await fetch(this.endpoint, {
method: 'POST',
headers: {
'content-type': 'application/json',
...attributionHeaders(),
},
body: JSON.stringify({ model: options.model, messages: options.messages }),
...options.signal ? { signal: options.signal } : {},
})
if (!response.ok) {
throw new LlmError(`Provider API error: ${response.status}`, 'PROVIDER_HTTP_ERROR', response.status)
}
// A real adapter parses the response and emits the complete chunk sequence.
yield { type: 'finish', reason: { kind: 'stop' } }
}
}
```
@@ -0,0 +1,185 @@
# LLM 适配器
[English](llm-adapter.md) | 中文
本文介绍如何为 Harness 接入一个新的 LLM 提供方。
## 概述
LLM 适配器是一个继承 `LlmAdapter` 的类,实现 `stream()` 方法,将 Harness 的统一请求格式转换为具体 API 的调用。
## 最小实现
```ts
import type { Context } from 'cordis'
import Schema from 'schemastery'
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. Convert options.messages to the provider format.
// 2. Call the streaming API.
// 3. Convert the response into StreamChunk values.
}
}
export interface Config {
apiKey: string
models: string[]
}
export const Config: Schema<Config> = Schema.object({
apiKey: Schema.string().required(),
models: Schema.array(Schema.string()).required(),
})
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
```ts
import { CallId, type StreamChunk } from '@deepseek-ai/dsh-llm'
async function* exampleChunks(): AsyncIterable<StreamChunk> {
// 1. Start each content block with block-start.
yield { type: 'block-start', index: 0, blockType: 'text' }
// 2. Stream text through text-delta.
yield { type: 'text-delta', index: 0, text: 'Hello' }
yield { type: 'text-delta', index: 0, text: ' world' }
// 3. End each content block with block-end and the complete block.
yield {
type: 'block-end',
index: 0,
block: { type: 'text', text: 'Hello world' },
}
// 4. Tool-call block.
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 usage.
yield { type: 'usage', usage: { inputTokens: 100, outputTokens: 50 } }
// 6. Finish reason.
yield { type: 'finish', reason: { kind: 'stop' } }
// Alternatively, { kind: 'tool-calls' } requests tool execution.
}
```
### 关键规则
- 每个 `block-start` 必须有对应的 `block-end`
- `index` 从 0 递增,标识内容块顺序
- `tool-call-delta``argumentsDelta` 是 JSON 字符串的增量(可以一次 yield 全部,也可以分多次)
- `finish` 必须是最后一个 chunk
- `usage``finish` 之前 yield
## GenerateOptions
`stream()` 接收仓库导出的 `GenerateOptions`。它包含模型名、对话历史、系统提示词、tool schema、生成参数、停止序列和中止信号;完整字段以 `@deepseek-ai/dsh-llm` 导出的 TypeScript 类型为准。适配器必须将支持的字段映射到具体 API;无法支持的字段应抛出带稳定 code 的 `LlmError`,不能静默丢弃。
## 注册适配器
```ts ignore-check
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 # References the model registered above.
```
## 实战参考
仓库中有两个完整实现可供参考:
- `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 序列。
## 错误处理
适配器应将传输和协议故障作为带稳定 code 的 `LlmError` 抛出;agent loop 会保留该错误及其 code,供诊断和策略使用。不要依赖普通 `Error` 被自动转换。每个提供方 HTTP 请求还必须合并 `attributionHeaders()`,并传递 `options.signal`。
```ts
import {
attributionHeaders,
LlmAdapter,
LlmError,
type GenerateOptions,
type StreamChunk,
} from '@deepseek-ai/dsh-llm'
class HttpAdapter extends LlmAdapter {
constructor(private readonly endpoint: string) {
super()
}
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
const response = await fetch(this.endpoint, {
method: 'POST',
headers: {
'content-type': 'application/json',
...attributionHeaders(),
},
body: JSON.stringify({ model: options.model, messages: options.messages }),
...options.signal ? { signal: options.signal } : {},
})
if (!response.ok) {
throw new LlmError(`Provider API error: ${response.status}`, 'PROVIDER_HTTP_ERROR', response.status)
}
// A real adapter parses the response and emits the complete chunk sequence.
yield { type: 'finish', reason: { kind: 'stop' } }
}
}
```
+6
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
config.md: 0cad49d9bd82813da230a44d526190d4e0b6a730
config.zh.md: ee1ea9a0c1d9010be8c1a3984acdcbe870e30584
+59
View File
@@ -0,0 +1,59 @@
# Configuration
English | [中文](config.zh.md)
Harness uses `cordis.yml` to describe which plugins an agent loads and the configuration passed to each one. The file composes capabilities; the generated configuration catalog records the fields and defaults each package actually supports, avoiding a second hand-maintained reference.
## Start from a real configuration
The repository examples are runnable configurations and the most reliable starting points for a new project:
- [echo-agent](../../../examples/echo-agent/cordis.yml) uses a local mock model and needs no API key.
- [coding-agent](../../../examples/coding-agent/cordis.yml) combines the DeepSeek model, Bash, filesystem, compaction, subagents, and workflows.
- [acp-agent](../../../examples/acp-agent/cordis.yml) connects to editor clients over ACP.
A minimal configuration is a list of plugin entries:
```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
```
## Plugin entries
`name` identifies an npm package or a local module relative to `cordis.yml`; `id` gives the plugin instance a stable identity; and `config` supplies plugin-specific configuration. Set `disabled: true` to skip an entry temporarily.
```yaml
- id: local-tool
name: './src/my-tool.ts'
disabled: false
config:
toolName: my_tool
```
Plugins load in file order. Place plugins that depend on services after the applications or capability plugins that provide them. Missing models, tools, and plugins fail as early as possible instead of being silently ignored.
## JavaScript values and environment variables
The Cordis loader evaluates runtime expressions tagged with `!!js`. Keep API keys and other secrets in the gitignored `.env` file at the repository root, never in committed configuration.
```yaml
config:
apiKey: !!js process.env.DEEPSEEK_API_KEY
cwd: !!js process.cwd()
```
The tag is `!!js`, not `!js`.
## Exact configuration reference
The generated [plugin configuration catalog](../../config-catalog.md) lists every current field, type, and default. For composition concepts, continue to the [architecture](../../architecture.md) and [capability interfaces](../../capability-seams.md). To create a configuration, copy the closest entry from the [examples overview](../../../examples/README.md) and adapt it.
@@ -1,14 +1,16 @@
# 配置文件
[English](config.md) | 中文
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 接入编辑器客户端。
- [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 接入编辑器客户端。
最小配置由一组插件条目组成:
@@ -54,4 +56,4 @@ config:
## 精确配置参考
每个插件当前支持的字段、类型和默认值见自动生成的[插件配置目录](../../../config-catalog.md)。理解插件如何组合可继续阅读[架构说明](../../../architecture.md)和[能力接口](../../../capability-seams.md);要创建自己的配置,优先复制并修改[示例目录说明](../../../../examples/README.md)中最接近的例子。
每个插件当前支持的字段、类型和默认值见自动生成的[插件配置目录](../../config-catalog.md)。理解插件如何组合可继续阅读[架构说明](../../architecture.md)和[能力接口](../../capability-seams.md);要创建自己的配置,优先复制并修改[示例目录说明](../../../examples/README.md)中最接近的例子。
+6
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: 4daf7fdc76b38b5d10cc8b4726d9b6239a72933f
index.zh.md: 1f41078822009234cbaf513aaa8dbc01c4b5d879
+49
View File
@@ -0,0 +1,49 @@
# Introduction
English | [中文](index.zh.md)
DeepSeek Harness is a **plugin-based agent development framework** built on the [Cordis](https://github.com/cordiverse/cordis) microkernel. Its central idea is simple: **everything is a plugin**.
## What it is
Harness implements every capability an AI agent needs—including LLM calls, tool execution, session management, and subtask delegation—as a composable plugin. A `cordis.yml` file declares which plugins to load and how to configure them, assembling a complete agent.
```yaml
# Select the LLM backend
- name: '@deepseek-ai/dsh-llm-deepseek'
config:
apiKey: !!js process.env.DEEPSEEK_API_KEY
# Select the application template
- name: '@deepseek-ai/dsh-stdio-agent'
config:
model: deepseek-v4-flash
```
## Who it is for
### Application users
To run an existing agent application, such as a coding assistant or conversational agent:
1. Copy an example template.
2. Add an API key.
3. Run it.
No code is required. See the [quick start](./quickstart.md).
### Plugin developers
To add a custom tool, a new LLM adapter, or another execution backend, write a plugin. Harness provides explicit extension interfaces and a type-safe development experience. See [development](../develop/basic/).
## Core features
- **Configuration only** — `cordis.yml` selects the capability set; changing a model or adding a tool is a configuration edit.
- **Hot replacement (HMR)** — edit plugin code during development without restarting the process.
## Technology
- **Runtime**: Node.js ^22.19 or >= 24
- **Language**: TypeScript (ESM)
- **Framework**: Cordis
- **Package manager**: pnpm workspaces (the repository pins pnpm 11)
@@ -1,5 +1,7 @@
# 介绍
[English](index.md) | 中文
DeepSeek Harness 是一个**插件化的 Agent 开发框架**,基于 [Cordis](https://github.com/cordiverse/cordis) 微内核构建。它的核心理念是:**一切皆插件**。
## 它是什么
@@ -7,12 +9,12 @@ DeepSeek Harness 是一个**插件化的 Agent 开发框架**,基于 [Cordis](
Harness 将一个 AI Agent(智能体) 所需要的所有能力——LLM 调用、工具执行、会话管理、子任务分配——全部构建为可组合的插件。你通过一个 `cordis.yml` 配置文件来声明加载哪些插件、使用什么参数,就能组装出一个完整的 Agent。
```yaml
# 选择 LLM 后端
# Select the LLM backend
- name: '@deepseek-ai/dsh-llm-deepseek'
config:
apiKey: !!js process.env.DEEPSEEK_API_KEY
# 选择应用模板
# Select the application template
- name: '@deepseek-ai/dsh-stdio-agent'
config:
model: deepseek-v4-flash
+6
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
quickstart.md: c2932cf5a5f49c3e3ddb887866b421c6e6cad7a1
quickstart.zh.md: 54ddb50be31203e98d4cd75d0b8851f8f0e14cc5
+99
View File
@@ -0,0 +1,99 @@
# Quick start
English | [中文](quickstart.zh.md)
This guide gets an agent running in five minutes.
## Prerequisites
- [Node.js](https://nodejs.org/) ^22.19 or >= 24
- [pnpm](https://pnpm.io/) 11 (use Corepack to select the repository-pinned version)
```sh
# Check versions
node -v # v22.19.x, or v24.x and newer
corepack enable
pnpm -v # 11.x
```
## Step 1: run echo-agent
echo-agent needs no API key and runs after dependencies are installed.
```sh
# Clone the repository
git clone https://github.com/deepseek-harness/deepseek-harness.git
cd deepseek-harness
# Install dependencies
pnpm install
# Start echo-agent
pnpm run demo:echo
```
The process prints:
```
echo-agent ready. Type a message ("echo <text>" triggers the tool).
>
```
Enter:
```
> echo hello world
```
The model issues a tool call, and the echo tool returns the text in uppercase:
```
[tool call] echo({"text":"hello world"})
[tool result] ECHO: HELLO WORLD
```
Your local environment is ready.
## Step 2: use a real model
Next, connect a real DeepSeek model and run the complete command-line agent.
### Get an API key
Get an API key from [DeepSeek Platform](https://platform.deepseek.com/).
### Configure the environment
Create a gitignored `.env` file in the repository root:
```sh
DEEPSEEK_API_KEY=sk-your-key-here
```
### Start coding-agent
```sh
pnpm run demo:repl
```
```
agent REPL ready. Give it a coding task.
>
```
This is a complete coding assistant that can read and write files, run commands, and delegate subtasks.
Try a task:
```
> Create hello.js in the current directory, print "Hello from Harness!", and run it
```
## What happened
echo-agent and coding-agent use the same application framework (`@deepseek-ai/dsh-stdio-agent`). Their `cordis.yml` files select different plugins and configuration. Custom agents use the same composition model.
## Next steps
- [Configuration](./config.md) — understand the `cordis.yml` format
- [Develop a plugin](../develop/basic/) — build your own tool or backend
@@ -1,5 +1,7 @@
# 快速开始
[English](quickstart.md) | 中文
本指南带你在 5 分钟内跑起一个 Agent。
## 环境准备
@@ -8,8 +10,8 @@
- [pnpm](https://pnpm.io/) 11(建议通过 Corepack 使用仓库固定的版本)
```sh
# 确认版本
node -v # v22.19.x,或 v24.x 及更高版本
# Check versions
node -v # v22.19.x, or v24.x and newer
corepack enable
pnpm -v # 11.x
```
@@ -19,14 +21,14 @@ pnpm -v # 11.x
echo-agent 不需要 API key,装好依赖就能跑。
```sh
# 克隆仓库
# Clone the repository
git clone https://github.com/deepseek-harness/deepseek-harness.git
cd deepseek-harness
# 安装依赖
# Install dependencies
pnpm install
# 启动 echo-agent
# Start echo-agent
pnpm run demo:echo
```
@@ -84,7 +86,7 @@ agent REPL ready. Give it a coding task.
试着给它一个任务:
```
> 在当前目录创建一个 hello.js,内容是打印 "Hello from Harness!",然后运行它
> Create hello.js in the current directory, print "Hello from Harness!", and run it
```
## 回头看
+6
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: e9a1f03785c7472c47550ec59ea0165d28d3d9a6
index.zh.md: 907f1452c9ff50d619989c18dcf2727addb2573d
+25
View File
@@ -0,0 +1,25 @@
---
layout: home
hero:
name: DeepSeek Harness
text: Plugin-based agent development framework
tagline: Built on the Cordis microkernel; everything is a plugin
actions:
- theme: brand
text: Quick start
link: /en/guide/quickstart
- theme: alt
text: Develop plugins
link: /en/develop/basic/
features:
- title: Plugin architecture
details: Built on the Cordis plugin system. Every capability is registered by a plugin, takes effect when loaded, and is reverted when unloaded.
- title: Configuration as composition
details: One cordis.yml determines the agent's complete capability set. Change a model or add a tool by editing configuration.
- title: Ready to use
details: Includes LLM calls, file access, Bash execution, subagent delegation, and the rest of the core toolchain. Copy a template to get started.
---
# DeepSeek Harness
English | [中文](index.zh.md)
@@ -19,3 +19,7 @@ features:
- title: 开箱即用
details: 内置 LLM 调用、文件读写、Bash 执行、子代理委派等完整工具链,复制模板即可运行。
---
# DeepSeek Harness
[English](index.md) | 中文
@@ -1,174 +0,0 @@
# 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
/** 取消或卸载时中止进行中的请求 */
signal?: AbortSignal
}
```
你的适配器需要将这些映射到具体 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, {
// ...method、headers 和 body
signal: options.signal,
})
if (!response.ok) {
throw new Error(`API error: ${response.status}`)
}
// ... 正常流式处理
}
```
+51 -3
View File
@@ -20,6 +20,7 @@ function fixture(): { root: string; pages: DocsPage[] } {
mkdirSync(join(root, 'packages'), { recursive: true })
writeFileSync(join(root, 'docs/a.md'), '# A\n')
writeFileSync(join(root, 'docs/b.md'), '# B\n')
writeFileSync(join(root, 'docs/x(y).md'), '# Parentheses\n')
writeFileSync(join(root, 'packages/tool.ts'), 'one\ntwo\n')
writeFileSync(join(root, 'packages/logo.svg'), '<svg/>\n')
return {
@@ -88,6 +89,46 @@ describe('rewriteMarkdown', () => {
})).toBe(source)
})
it('replaces the destination token without changing repeated titles or escapes', () => {
const { root, pages } = fixture()
const source = '[title](b.md "b.md") [escaped](x\\(y\\).md)\n'
expect(rewriteMarkdown(source, {
locale: 'en',
sourcePath: 'docs/a.md',
route: 'en/a.md',
pages,
repoRoot: root,
repositoryRef: 'abc123',
})).toBe(
'[title](./reference/b.md "b.md") '
+ '[escaped](https://github.com/deepseek-harness/deepseek-harness/blob/abc123/docs/x(y).md)\n',
)
})
it('routes a pair switcher across locales while ordinary links stay in locale', () => {
const { root, pages } = fixture()
writeFileSync(join(root, 'docs/a.zh.md'), '# A\n')
const paired = pages.filter(page => page.source !== 'docs/a.md')
paired.push(
{
locale: 'root', contentLocale: 'zh-CN', source: 'docs/a.zh.md', sourceAliases: ['docs/a.md'],
route: 'guide/a.md', label: 'A', sidebar: 'zh-guide', section: 'Test', order: 1,
},
{
locale: 'en', contentLocale: 'en-US', source: 'docs/a.md', sourceAliases: ['docs/a.zh.md'],
route: 'en/guide/a.md', label: 'A', sidebar: 'en-guide', section: 'Test', order: 1,
},
)
expect(rewriteMarkdown('[English](a.md) [B](b.md)\n', {
locale: 'root',
sourcePath: 'docs/a.zh.md',
route: 'guide/a.md',
pages: paired,
repoRoot: root,
repositoryRef: 'abc123',
})).toBe('[English](../en/guide/a.md) [B](../reference-root/b.md)\n')
})
it('fails loud when a relative target is missing', () => {
const { root, pages } = fixture()
expect(() => rewriteMarkdown('[missing](missing.md)\n', {
@@ -102,14 +143,21 @@ describe('rewriteMarkdown', () => {
})
describe('docsPages locale routes', () => {
it('publishes the same canonical source at every corresponding locale route', () => {
it('publishes every route in both locales and selects paired user sources', () => {
const byRoute = new Map(docsPages.map(page => [page.route, page]))
for (const page of docsPages.filter(page => page.locale === 'root')) {
const counterpart = byRoute.get(`en/${page.route}`)
expect(counterpart, page.route).toBeDefined()
expect(counterpart?.locale).toBe('en')
expect(counterpart?.source).toBe(page.source)
expect(counterpart?.contentLocale).toBe(page.contentLocale)
if (page.source.startsWith('docs/user/')) {
expect(page.source).toMatch(/\.zh\.md$/)
expect(page.contentLocale).toBe('zh-CN')
expect(counterpart?.source).toBe(page.source.replace(/\.zh\.md$/, '.md'))
expect(counterpart?.contentLocale).toBe('en-US')
} else {
expect(counterpart?.source).toBe(page.source)
expect(counterpart?.contentLocale).toBe(page.contentLocale)
}
}
})
})
+91 -8
View File
@@ -23,6 +23,13 @@ interface Replacement {
value: string
}
interface DestinationRange {
start: number
end: number
}
type RewritableNode = Extract<Nodes, { type: 'link' | 'image' | 'definition' }>
/** Inputs for rewriting one canonical Markdown page. */
export interface RewriteMarkdownOptions {
locale: DocsLocale
@@ -44,6 +51,75 @@ function isExternalOrSiteAbsolute(url: string): boolean {
|| /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)
}
function skipWhitespace(source: string, start: number): number {
let index = start
while (/\s/.test(source[index] ?? '')) index += 1
return index
}
function labelEnd(source: string): number {
const first = source.indexOf('[')
if (first === -1) return -1
let depth = 0
for (let index = first; index < source.length; index += 1) {
const char = source[index]
if (char === '\\') {
index += 1
} else if (char === '[') {
depth += 1
} else if (char === ']') {
depth -= 1
if (depth === 0) return index
}
}
return -1
}
function destinationRange(rawNode: string, type: 'link' | 'image' | 'definition'): DestinationRange {
const endOfLabel = labelEnd(rawNode)
if (endOfLabel === -1) {
throw new Error(`project-doc-site: cannot locate label end in ${JSON.stringify(rawNode)}.`)
}
let start: number
if (type === 'definition') {
const colon = rawNode.indexOf(':', endOfLabel + 1)
if (colon === -1) {
throw new Error(`project-doc-site: cannot locate definition separator in ${JSON.stringify(rawNode)}.`)
}
start = skipWhitespace(rawNode, colon + 1)
} else {
if (rawNode[endOfLabel + 1] !== '(') {
throw new Error(`project-doc-site: cannot locate inline destination in ${JSON.stringify(rawNode)}.`)
}
start = skipWhitespace(rawNode, endOfLabel + 2)
}
if (rawNode[start] === '<') {
for (let index = start + 1; index < rawNode.length; index += 1) {
if (rawNode[index] === '\\') index += 1
else if (rawNode[index] === '>') return { start: start + 1, end: index }
}
throw new Error(`project-doc-site: cannot locate angle-bracket destination end in ${JSON.stringify(rawNode)}.`)
}
let depth = 0
for (let index = start; index < rawNode.length; index += 1) {
const char = rawNode[index]
if (char === '\\') {
index += 1
} else if (char === '(') {
depth += 1
} else if (char === ')') {
if (depth === 0) return { start, end: index }
depth -= 1
} else if (/\s/.test(char ?? '') && depth === 0) {
return { start, end: index }
}
}
return { start, end: rawNode.length }
}
function splitTarget(url: string): { path: string; suffix: string } {
const boundary = url.search(/[?#]/)
if (boundary === -1) return { path: url, suffix: '' }
@@ -78,6 +154,12 @@ function sourceMap(pages: DocsPage[]): Map<string, Map<DocsLocale, DocsPage>> {
return map
}
function counterpartSource(source: string): string {
return source.endsWith('.zh.md')
? source.replace(/\.zh\.md$/, '.md')
: source.replace(/\.md$/, '.zh.md')
}
function resolveRepositoryTarget(sourceAbs: string, rawPath: string, repoRoot: string): { absPath: string; line?: number } {
const decoded = decodePath(rawPath)
let absPath = resolve(dirname(sourceAbs), decoded)
@@ -129,13 +211,17 @@ export function rewriteMarkdown(source: string, options: RewriteMarkdownOptions)
const tree = fromMarkdown(source, { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] })
const replacements: Replacement[] = []
const rewrite = (node: Nodes & { url: string }): void => {
const rewrite = (node: RewritableNode): void => {
if (isExternalOrSiteAbsolute(node.url)) return
const { path, suffix } = splitTarget(node.url)
if (path === '') return
const { absPath, line } = resolveRepositoryTarget(sourceAbs, path, options.repoRoot)
const targetPath = repoPath(absPath, options.repoRoot)
const page = published.get(targetPath)?.get(options.locale)
const isLanguageSwitcher = targetPath === counterpartSource(options.sourcePath)
const targetLocale: DocsLocale = isLanguageSwitcher
? options.locale === 'root' ? 'en' : 'root'
: options.locale
const page = published.get(targetPath)?.get(targetLocale)
const nextUrl = page === undefined
? githubTarget(absPath, line, suffix, options.repositoryRef, options.repoRoot, node.type === 'image')
: routeTarget(options.route, page.route, suffix)
@@ -146,13 +232,10 @@ export function rewriteMarkdown(source: string, options: RewriteMarkdownOptions)
throw new Error(`project-doc-site: link ${JSON.stringify(node.url)} has no source offsets.`)
}
const rawNode = source.slice(start, end)
const urlOffset = rawNode.lastIndexOf(node.url)
if (urlOffset === -1) {
throw new Error(`project-doc-site: cannot locate raw target ${JSON.stringify(node.url)} in ${JSON.stringify(rawNode)}.`)
}
const rawDestination = destinationRange(rawNode, node.type)
replacements.push({
start: start + urlOffset,
end: start + urlOffset + node.url.length,
start: start + rawDestination.start,
end: start + rawDestination.end,
value: nextUrl,
})
}
+12
View File
@@ -11,6 +11,18 @@
"docs/development.md",
"docs/i18n/README.md",
"docs/i18n/translation-rules.md",
"docs/user/develop/basic/config.md",
"docs/user/develop/basic/index.md",
"docs/user/develop/basic/tool.md",
"docs/user/develop/framework/events.md",
"docs/user/develop/framework/index.md",
"docs/user/develop/framework/service.md",
"docs/user/develop/practice/index.md",
"docs/user/develop/practice/llm-adapter.md",
"docs/user/guide/config.md",
"docs/user/guide/index.md",
"docs/user/guide/quickstart.md",
"docs/user/index.md",
"docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md",
"docs/rfc/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md",
"python/README.md",
+66 -44
View File
@@ -42,63 +42,93 @@ export interface DocsPage {
}
interface MirroredPage {
source: string
source: string | Record<DocsLocale, string>
route: string
contentLocale: DocsPage['contentLocale']
contentLocale: DocsPage['contentLocale'] | Record<DocsLocale, DocsPage['contentLocale']>
label: Record<DocsLocale, string>
sidebar: Record<DocsLocale, DocsSidebar | null>
section: Record<DocsLocale, string>
order: number
sourceAliases?: string[] | Partial<Record<DocsLocale, string[]>>
}
type PairedPage = Omit<MirroredPage, 'source' | 'contentLocale' | 'sourceAliases'> & {
/** English side of a sibling `foo.md` / `foo.zh.md` pair. */
source: string
/** Language-neutral repository aliases, such as the directory of an index page. */
sourceAliases?: string[]
}
function mirroredPages(pages: MirroredPage[]): DocsPage[] {
return pages.flatMap(page => (['root', 'en'] as const).map(locale => ({
locale,
contentLocale: page.contentLocale,
source: page.source,
route: locale === 'root' ? page.route : `en/${page.route}`,
label: page.label[locale],
sidebar: page.sidebar[locale],
section: page.section[locale],
order: page.order,
...(page.sourceAliases === undefined ? {} : { sourceAliases: page.sourceAliases }),
})))
function localized<T>(value: T | Record<DocsLocale, T>, locale: DocsLocale): T {
return typeof value === 'object' && value !== null && !Array.isArray(value)
? (value as Record<DocsLocale, T>)[locale]
: value
}
const homeAndGuide = mirroredPages([
function mirroredPages(pages: MirroredPage[]): DocsPage[] {
return pages.flatMap(page => (['root', 'en'] as const).map((locale) => {
const aliases = page.sourceAliases === undefined
? undefined
: Array.isArray(page.sourceAliases) ? page.sourceAliases : page.sourceAliases[locale]
return {
locale,
contentLocale: localized(page.contentLocale, locale),
source: localized(page.source, locale),
route: locale === 'root' ? page.route : `en/${page.route}`,
label: page.label[locale],
sidebar: page.sidebar[locale],
section: page.section[locale],
order: page.order,
...(aliases === undefined ? {} : { sourceAliases: aliases }),
}
}))
}
function pairedPages(pages: PairedPage[]): DocsPage[] {
return mirroredPages(pages.map((page) => {
const chineseSource = page.source.replace(/\.md$/, '.zh.md')
const sharedAliases = page.sourceAliases ?? []
return {
...page,
source: { root: chineseSource, en: page.source },
contentLocale: { root: 'zh-CN', en: 'en-US' },
sourceAliases: {
root: [...sharedAliases, page.source],
en: [...sharedAliases, chineseSource],
},
}
}))
}
const homeAndGuide = pairedPages([
{
source: 'docs/user/zh-CN/index.md',
source: 'docs/user/index.md',
route: 'index.md',
contentLocale: 'zh-CN',
label: { root: 'DeepSeek Harness', en: 'DeepSeek Harness' },
sidebar: { root: null, en: null },
section: { root: '首页', en: 'Home' },
order: 0,
},
{
source: 'docs/user/zh-CN/guide/index.md',
source: 'docs/user/guide/index.md',
route: 'guide/index.md',
contentLocale: 'zh-CN',
label: { root: '介绍', en: 'Introduction' },
sidebar: { root: 'zh-guide', en: 'en-guide' },
section: { root: '入门', en: 'Guide' },
order: 1,
sourceAliases: ['docs/user/zh-CN/guide'],
sourceAliases: ['docs/user/guide'],
},
{
source: 'docs/user/zh-CN/guide/quickstart.md',
source: 'docs/user/guide/quickstart.md',
route: 'guide/quickstart.md',
contentLocale: 'zh-CN',
label: { root: '快速开始', en: 'Quick start' },
sidebar: { root: 'zh-guide', en: 'en-guide' },
section: { root: '入门', en: 'Guide' },
order: 2,
},
{
source: 'docs/user/zh-CN/guide/config.md',
source: 'docs/user/guide/config.md',
route: 'guide/config.md',
contentLocale: 'zh-CN',
label: { root: '配置文件', en: 'Configuration' },
sidebar: { root: 'zh-guide', en: 'en-guide' },
section: { root: '入门', en: 'Guide' },
@@ -106,77 +136,69 @@ const homeAndGuide = mirroredPages([
},
])
const develop = mirroredPages([
const develop = pairedPages([
{
source: 'docs/user/zh-CN/develop/basic/index.md',
source: 'docs/user/develop/basic/index.md',
route: 'develop/basic/index.md',
contentLocale: 'zh-CN',
label: { root: '第一个插件', en: 'First plugin' },
sidebar: { root: 'zh-develop', en: 'en-develop' },
section: { root: '基础', en: 'Basics' },
order: 1,
sourceAliases: ['docs/user/zh-CN/develop/basic'],
sourceAliases: ['docs/user/develop/basic'],
},
{
source: 'docs/user/zh-CN/develop/basic/tool.md',
source: 'docs/user/develop/basic/tool.md',
route: 'develop/basic/tool.md',
contentLocale: 'zh-CN',
label: { root: '开发一个 Tool', en: 'Build a tool' },
sidebar: { root: 'zh-develop', en: 'en-develop' },
section: { root: '基础', en: 'Basics' },
order: 2,
},
{
source: 'docs/user/zh-CN/develop/basic/config.md',
source: 'docs/user/develop/basic/config.md',
route: 'develop/basic/config.md',
contentLocale: 'zh-CN',
label: { root: '插件配置', en: 'Plugin configuration' },
sidebar: { root: 'zh-develop', en: 'en-develop' },
section: { root: '基础', en: 'Basics' },
order: 3,
},
{
source: 'docs/user/zh-CN/develop/framework/index.md',
source: 'docs/user/develop/framework/index.md',
route: 'develop/framework/index.md',
contentLocale: 'zh-CN',
label: { root: '插件与生命周期', en: 'Plugin lifecycle' },
sidebar: { root: 'zh-develop', en: 'en-develop' },
section: { root: '框架能力', en: 'Framework' },
order: 1,
sourceAliases: ['docs/user/zh-CN/develop/framework'],
sourceAliases: ['docs/user/develop/framework'],
},
{
source: 'docs/user/zh-CN/develop/framework/service.md',
source: 'docs/user/develop/framework/service.md',
route: 'develop/framework/service.md',
contentLocale: 'zh-CN',
label: { root: '服务与依赖', en: 'Services and dependencies' },
sidebar: { root: 'zh-develop', en: 'en-develop' },
section: { root: '框架能力', en: 'Framework' },
order: 2,
},
{
source: 'docs/user/zh-CN/develop/framework/events.md',
source: 'docs/user/develop/framework/events.md',
route: 'develop/framework/events.md',
contentLocale: 'zh-CN',
label: { root: '事件系统', en: 'Event system' },
sidebar: { root: 'zh-develop', en: 'en-develop' },
section: { root: '框架能力', en: 'Framework' },
order: 3,
},
{
source: 'docs/user/zh-CN/develop/practice/index.md',
source: 'docs/user/develop/practice/index.md',
route: 'develop/practice/index.md',
contentLocale: 'zh-CN',
label: { root: '能力的三层拆分', en: 'Capability layering' },
sidebar: { root: 'zh-develop', en: 'en-develop' },
section: { root: '实战', en: 'Practice' },
order: 1,
sourceAliases: ['docs/user/zh-CN/develop/practice'],
sourceAliases: ['docs/user/develop/practice'],
},
{
source: 'docs/user/zh-CN/develop/practice/llm-adapter.md',
source: 'docs/user/develop/practice/llm-adapter.md',
route: 'develop/practice/llm-adapter.md',
contentLocale: 'zh-CN',
label: { root: 'LLM 适配器', en: 'LLM adapter' },
sidebar: { root: 'zh-develop', en: 'en-develop' },
section: { root: '实战', en: 'Practice' },