website: wire the site into the repo gates; make every tutorial example compile

- website joins the pnpm workspace; root scripts website:dev/website:build;
  run-gates gains a website-build gate (ci-primary + ci-static) — the
  VitePress build doubles as the site's dead-link check; AGENTS.md documents
  the commands.
- doc-typecheck + verify-type-equiv now scan website/zh-CN/**/*.md; every
  ```typescript fence converted to ```ts and made standalone-compilable
  (55 compiled, 1 ignore-check). Phantom APIs the compiler caught are fixed:
  invented event names (agent/turn-end, tool/call, llm/pre-request, ready,
  dispose) replaced with real catalog events or per-plugin declare-module
  merges; presentCall/inject/Config claims corrected to the real shapes.
- guide/config.md entry-fields table completed against loader EntryOptions;
  its coding-agent example brought in line with examples/coding-agent.
This commit is contained in:
lintianle
2026-07-16 18:12:22 +08:00
parent 4ce98cbab3
commit 6ce9f16030
21 changed files with 1949 additions and 227 deletions

View File

@@ -30,6 +30,7 @@ packages/ Harness packages at packages/<group>/<pkg>/, all named @deepseek-ai
examples/ Runnable demos: thin cordis.yml leaves over the app packages (see examples/AGENTS.md)
docs/ architecture, generated catalogs, RFCs, postmortems, cookbook (see docs/AGENTS.md)
scripts/ repo gates and generators
website/ VitePress docs site (zh-CN)
```
Per-package map: the group READMEs, indexed from [packages/README.md](packages/README.md).
@@ -48,6 +49,7 @@ pnpm run lint
pnpm run build # tsc emits lib/types, tsdown bundles runtime
pnpm run hygiene # knip + publint + workspace constraints + NodeNext consumer check
pnpm run doc-sync # all documentation gates; see the doc-sync script in package.json
pnpm run website:build # VitePress build (doubles as the site's dead-link check)
pnpm run demo:echo # mock-model REPL, no key needed
pnpm run demo:repl # real REPL coding agent (needs DEEPSEEK_API_KEY)
pnpm run demo:cordis # self-referential demo: the agent modifies its own runtime (needs key)
@@ -65,6 +67,7 @@ pnpm run lint
pnpm run test:coverage
pnpm run test:snapshot
pnpm run doc-sync
pnpm run website:build
pnpm run verify-module-graph
pnpm run build
pnpm run hygiene

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"exclude": ["duplicates"],
"ignoreWorkspaces": ["vendor/*"],
"ignoreWorkspaces": ["vendor/*", "website"],
"workspaces": {
".": {
"entry": [

View File

@@ -9,7 +9,8 @@
},
"workspaces": [
"vendor/*",
"packages/*/*"
"packages/*/*",
"website"
],
"scripts": {
"build": "tsc -b tsconfig.build.json && tsdown",
@@ -60,6 +61,8 @@
"verify-persistence-catalog": "tsx scripts/gen-persistence-catalog.ts --check",
"gen-module-graph": "tsx scripts/gen-module-graph.ts",
"verify-module-graph": "tsx scripts/gen-module-graph.ts --check",
"website:dev": "pnpm --filter @deepseek-ai/website run dev",
"website:build": "pnpm --filter @deepseek-ai/website run build",
"constraints": "tsx scripts/check-workspace-constraints.ts",
"doc-sync": "pnpm run doc-typecheck && pnpm run verify-cordis-catalog && pnpm run verify-cordis-api && pnpm run verify-export-jsdoc && pnpm run verify-tool-catalog && pnpm run verify-config-catalog && pnpm run verify-persistence-catalog && pnpm run verify-doc-graphs && pnpm run verify-md-wrap && pnpm run verify-md-links && pnpm run verify-doc-refs && pnpm run verify-package-paths && pnpm run verify-mermaid && pnpm run verify-rfc-classification && pnpm run verify-rfc-format && pnpm run verify-type-equiv && pnpm run verify-translation-pairing && pnpm run verify-doc-budgets",
"hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-node-next-types",
@@ -74,12 +77,14 @@
"@agentclientprotocol/sdk": "0.25.1",
"@stylistic/eslint-plugin": "^5.10.0",
"@types/jsdom": "^28.0.3",
"@types/js-yaml": "^4.0.9",
"@types/mdast": "^4.0.4",
"@types/node": "^22.20.0",
"@vitest/coverage-v8": "^4.1.8",
"eslint": "^10.4.1",
"fast-check": "^4.8.0",
"jsdom": "29.1.1",
"js-yaml": "^4.1.0",
"knip": "^6.16.1",
"lefthook": "^2.1.9",
"mdast-util-from-markdown": "^2.0.3",

1448
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
packages:
- vendor/*
- packages/*/*
- website
peerDependencyRules:
allowedVersions:

View File

@@ -2,7 +2,8 @@
* Doc-sync gate (doc-sync-enforcement RFC, part 1): typecheck the fenced `ts` code blocks in our
* Markdown so documentation can't drift from the API it documents.
*
* Every ```ts block in README.md, docs/** and packages/* /README.md is
* Every ```ts block in README.md, docs/**, packages/* /README.md and the
* website tutorial pages (website/zh-CN/**) is
* extracted to a temp typecheck project and compiled against the workspace
* sources through the same project-reference boundaries used by repo
* typecheck. A block that is a deliberate sketch rather than compilable code
@@ -134,7 +135,7 @@ function tempTsconfig(): string {
})
}
const markdownGlobs = ['README.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md']
const markdownGlobs = ['README.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md', 'website/zh-CN/**/*.md']
const files: string[] = []
for (const pattern of markdownGlobs) {

View File

@@ -167,6 +167,7 @@ function ciPrimaryGates(): Gate[] {
...docSyncLeafGates(),
pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }),
pnpmScript('knip', 'knip'),
pnpmScript('website-build', 'website:build', { label: 'website build' }),
pnpmScript('build', 'build', { needs: ['typecheck'] }),
pnpmScript('publint', 'publint', { needs: ['build'] }),
pnpmScript('node-next-types', 'verify-node-next-types', {
@@ -184,6 +185,7 @@ function ciStaticGates(): Gate[] {
...docSyncLeafGates(),
pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }),
pnpmScript('knip', 'knip'),
pnpmScript('website-build', 'website:build', { label: 'website build' }),
]
}

View File

@@ -35,7 +35,7 @@ const root = resolve(import.meta.dirname, '..')
* added to a doc with NO manifest entry is still discovered here and reported as
* an orphan, instead of being silently skipped.
*/
const MARKDOWN_GLOBS = ['README.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md']
const MARKDOWN_GLOBS = ['README.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md', 'website/zh-CN/**/*.md']
/** One manifest entry: a documented type-equiv block and its source symbol. */
interface ManifestEntry {

View File

@@ -55,16 +55,21 @@ Cordis 同时解决了上述两个问题:
DeepSeek Harness 将 Cordis 的可组合性应用到 Agent 开发领域:
```typescript
```ts
import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import type {} from '@deepseek-ai/dsh-llm'
// 一个 Harness 插件天然是可逆的
export const inject = ['tools', 'llm'] // 空间可组合:声明依赖
export function apply(ctx: Context) {
// 时间可组合:注册会被自动追踪和回收
ctx.tools.register(defineTool('my-tool', {
ctx.tools.register(defineTool({
name: 'my-tool',
description: '...',
parameters: { /* ... */ },
async execute(args) { /* ... */ },
async execute(args) { return [] },
}))
}
```

View File

@@ -50,9 +50,14 @@ Root Context
- 因此服务的提供被记录在作用上下文中
- 上下文将作用与余作用关联起来,提供了统一的时间、空间可组合性
```typescript
```ts
import { Service, type Context } from 'cordis'
// 提供服务 = 一个 effect占用 ctx.llm 这个 "资源"
class LlmService extends Service {
constructor(ctx: Context) {
super(ctx, 'llm')
}
// 当此插件卸载时ctx.llm 被回收effect 的逆操作)
// 所有依赖 llm 的插件因 coeffect 不满足而挂起
}
@@ -66,7 +71,16 @@ class LlmService extends Service {
框架将领域中的所有方法都封装为 effect 版本。开发者只需调用 `ctx` 上的方法,就能自动获得时间/空间可组合性:
```typescript
```ts
import type { Context } from 'cordis'
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
import type { LlmAdapter, Message } from '@deepseek-ai/dsh-llm'
import type { Agent } from '@deepseek-ai/dsh-agent'
declare function validateResult(agent: Agent, turn: number, step: number, message: Message, next: () => Promise<Message>): Promise<Message>
declare const myTool: ToolDefinition
declare const adapter: LlmAdapter
export function apply(ctx: Context) {
// 以下每一行都是 effect——卸载时自动逆序回收
ctx.on('agent/step-result', validateResult)
@@ -82,7 +96,16 @@ export function apply(ctx: Context) {
可以逐步将现有框架中的 API 替换为可组合版本,无需一次性重写:
```typescript
```ts
import type { Context } from 'cordis'
declare const ctx: Context
declare function handler(): void
declare const legacySystem: {
register(handler: () => void): object
unregister(token: object): void
}
// 第一步:用 ctx.effect 包装遗留 API
ctx.effect(() => {
const legacy = legacySystem.register(handler)

View File

@@ -24,13 +24,19 @@ Cordis 将程序中的资源依赖抽象为**服务** (service)
- 运行时对依赖不满足的插件**等待**,而非拒绝
- 服务生命周期结束前,依赖该服务的插件**先一步被回收**
```typescript
```ts
import { Service, type Context } from 'cordis'
// LLM 适配器插件:提供 llm 服务
export class LlmService extends Service {
static inject = ['http'] // 自身依赖 http
// 当 http 不可用时LlmService 自动挂起
// 挂起导致 ctx.llm 不可用
// 所有 inject: ['llm'] 的插件级联挂起
constructor(ctx: Context) {
super(ctx, 'llm')
}
}
```
@@ -57,7 +63,11 @@ export class LlmService extends Service {
## 在 Cordis 中的实现
```typescript
```ts
import type { Context } from 'cordis'
import type {} from '@deepseek-ai/dsh-tools'
import type {} from '@deepseek-ai/dsh-llm'
// 声明依赖
export const inject = ['tools', 'llm']
@@ -85,6 +95,6 @@ llm service 恢复 → 依赖 llm 的插件重新 PENDING → ACTIVE
| LLM adapter 热替换 | 依赖 `llm` 的插件自动挂起/恢复,中间不丢状态 |
| 按需加载 bash 执行器 | bash tool 只在 `bash` 服务就绪后注册 |
| 子 Agent 独立服务空间 | 通过 `ctx.isolate()` 隔离服务实例,互不干扰 |
| 可选能力降级 | `inject: { web: { required: false } }` 允许 web 不可用时继续运行 |
| 可选能力降级 | 不声明 `inject`,用 `ctx.get('web')` 读取——服务不可用时返回 `undefined`,插件照常运行 |
这意味着 Harness 插件开发者无需编写防御性的 "if service exists" 检查——框架保证:当你的 `apply` 被调用时,声明的依赖一定已就绪。

View File

@@ -103,7 +103,20 @@ $$
| $\text{restore}$ | `fiber.dispose()` | 执行 Fiber 的整个回收链 |
| $f^{-1}$ | dispose 返回值 / cleanup 函数 | 逆操作 |
```typescript
```ts
import type { Context } from 'cordis'
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
declare module 'cordis' {
interface Events {
'my-plugin/event'(): void
}
}
declare function startServer(port: number): { close(): void }
declare function handler(): void
declare const myTool: ToolDefinition
export function apply(ctx: Context) {
// effect: 创建资源,返回其逆操作
ctx.effect(() => {
@@ -112,7 +125,7 @@ export function apply(ctx: Context) {
})
// 框架 API 内部已封装 effect
ctx.on('event', handler) // 内部: effect(addListener, removeListener)
ctx.on('my-plugin/event', handler) // 内部: effect(addListener, removeListener)
ctx.tools.register(myTool) // 内部: effect(addTool, removeTool)
}
// 当此插件被卸载时restore 自动按逆序执行所有 f⁻¹

View File

@@ -4,27 +4,21 @@
## 定义 Config 类型
在插件中导出一个 `Config` 类型和可选的默认值
在插件中导出一个 `Config` 类型`apply` 的第二个参数就是用户配置
```typescript
```ts
import type { Context } from 'cordis'
export const name = 'my-plugin'
export interface Config {
greeting: string
maxRetries: number
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) // 用户配置或默认值
console.log(config.greeting ?? 'Hello') // 用户配置或默认值
}
```
@@ -37,32 +31,32 @@ export function apply(ctx: Context, config: Config) {
maxRetries: 5
```
未提供的字段使用导出的 `Config` 对象中的默认值
只导出类型时,配置原样传入,默认值由代码自己兜底(如上面的 `??`)。想让框架代管默认值和校验,导出一个 schema见下节
## Schema 校验
对于需要严格校验的场景,使用 Schemastery 定义 schema
对于需要默认值和严格校验的场景,额外导出一个 Schemastery schema(仓库约定以 `z` 引入)。加载时框架先用它校验并填充默认值,再把结果传给 `apply`
```typescript
```ts
import type { Context } from 'cordis'
import Schema from 'schemastery'
import z from 'schemastery'
export const name = 'validated-plugin'
export interface Config {
apiKey: string
timeout: number
mode: 'fast' | 'accurate'
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 const Config: z<Config> = z.object({
apiKey: z.string().required(),
timeout: z.number().default(30000),
mode: z.union(['fast', 'accurate'] as const).default('fast'),
})
export function apply(ctx: Context, config: Config) {
// config 已经过校验,类型安全
// config 已经过校验,类型安全,默认值已填充
}
```
@@ -74,13 +68,14 @@ Schema 在插件加载时执行校验。如果配置不合法,插件会加载
Harness 的约定:**任何两个部署可能想要不同值的东西,都应该是配置字段**。
```typescript
```ts
// 错误 — 硬编码超时时间
const TIMEOUT = 30000
// 正确 — 可配置
export interface Config {
timeoutMs: number // 默认 30000
/** 默认 30000 */
timeoutMs?: number
}
```
@@ -90,9 +85,16 @@ export interface Config {
如果配置引用了不存在的东西(比如一个不存在的模型名),应该尽早报错,而不是静默跳过:
```typescript
```ts
import type { Context } from 'cordis'
import type {} from '@deepseek-ai/dsh-llm'
export interface Config {
model: string
}
export function apply(ctx: Context, config: Config) {
if (!ctx.llm.hasAdapter(config.model)) {
if (!ctx.llm.models().includes(config.model)) {
throw new Error(`Model "${config.model}" is not registered by any LLM adapter`)
}
}

View File

@@ -6,7 +6,7 @@
在 Harness 中,插件是一个导出 `apply` 函数的 TypeScript 模块。框架在加载时调用 `apply`,传入一个 `ctx`(上下文对象),你通过 `ctx` 注册能力:
```typescript
```ts
import type { Context } from 'cordis'
export const name = 'my-plugin'
@@ -22,16 +22,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) {
// 监听 agent-loop 的 ready 事件
ctx.on('ready', () => {
console.log('[hello-plugin] 插件已加载!')
})
// apply 函数体在插件加载时执行
console.log('[hello-plugin] 插件已加载!')
}
```
@@ -52,7 +50,9 @@ export function apply(ctx: Context) {
如果你有需要手动清理的资源(比如一个网络连接),用 `ctx.effect()` 告诉框架怎么清理:
```typescript
```ts
import type { Context } from 'cordis'
export function apply(ctx: Context) {
ctx.effect(() => {
const timer = setInterval(() => {
@@ -69,13 +69,23 @@ export function apply(ctx: Context) {
如果你的插件需要使用其他服务(如 `tools``llm`),需要声明 `inject`
```typescript
```ts
import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'my-tool-plugin'
export const inject = ['tools']
export function apply(ctx: Context) {
// ctx.tools 现在可用
ctx.tools.register(/* ... */)
ctx.tools.register(defineTool({
name: 'demo',
description: 'Demo tool.',
parameters: {},
async execute() {
return []
},
}))
}
```
@@ -87,7 +97,10 @@ export function apply(ctx: Context) {
### 对象形式
```typescript
```ts
import type { Context } from 'cordis'
import type {} from '@deepseek-ai/dsh-tools'
export default {
name: 'my-plugin',
inject: ['tools'],
@@ -99,8 +112,9 @@ export default {
### 类形式
```typescript
import { Service } from 'cordis'
```ts
import { Service, type Context } from 'cordis'
import type {} from '@deepseek-ai/dsh-tools'
export default class MyService extends Service {
static inject = ['tools']
@@ -109,8 +123,9 @@ export default class MyService extends Service {
super(ctx, 'myService')
}
start() {
// 服务启动逻辑
// 服务的公开方法
greet(name: string) {
return `Hello, ${name}!`
}
}
```
@@ -121,7 +136,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'

View File

@@ -4,7 +4,7 @@ Tool 是模型可以调用的能力。本文介绍如何用 `defineTool` 编写
## 最小示例
```typescript
```ts
import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
@@ -32,28 +32,34 @@ export function apply(ctx: Context) {
### 基本类型
```typescript
parameters: {
```ts
import type { SchemaSpec } from '@deepseek-ai/dsh-tools'
const parameters = {
path: { type: 'string', required: true },
limit: { type: 'number' },
recursive: { type: 'boolean' },
}
} satisfies SchemaSpec
// 推导类型: { path: string; limit?: number; recursive?: boolean }
```
### 枚举
```typescript
parameters: {
```ts
import type { SchemaSpec } from '@deepseek-ai/dsh-tools'
const parameters = {
mode: { type: 'string', required: true, enum: ['read', 'write', 'append'] },
}
} satisfies SchemaSpec
// 推导类型: { mode: string } (运行时校验 enum 值)
```
### 嵌套对象
```typescript
parameters: {
```ts
import type { SchemaSpec } from '@deepseek-ai/dsh-tools'
const parameters = {
options: {
type: 'object',
properties: {
@@ -61,19 +67,21 @@ parameters: {
retries: { type: 'number' },
},
},
}
} satisfies SchemaSpec
// 推导类型: { options?: { timeout?: number; retries?: number } }
```
### 数组
```typescript
parameters: {
```ts
import type { SchemaSpec } from '@deepseek-ai/dsh-tools'
const parameters = {
tags: {
type: 'array',
items: { type: 'string' },
},
}
} satisfies SchemaSpec
// 推导类型: { tags?: string[] }
```
@@ -92,29 +100,44 @@ 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' }]
}
defineTool({
name: 'demo',
description: 'Demo tool.',
parameters: {},
async execute(args, exec) {
// args: 根据 parameters 自动推导的类型
// exec: ToolExecution 对象,提供执行上下文
// 返回 ContentBlock 数组
return [{ type: 'text', text: 'result here' }]
},
})
```
### 返回值
`execute` 必须返回一个 `ContentBlock[]`,告诉模型 tool 的执行结果:
```typescript
```ts
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
declare const matchResults: string[]
// 文本结果
return [{ type: 'text', text: 'file content here...' }]
function textResult(): ContentBlock[] {
return [{ type: 'text', text: 'file content here...' }]
}
// 多个 block
return [
{ type: 'text', text: 'Found 3 matches:' },
{ type: 'text', text: matchResults.join('\n') },
]
function multiBlockResult(): ContentBlock[] {
return [
{ type: 'text', text: 'Found 3 matches:' },
{ type: 'text', text: matchResults.join('\n') },
]
}
```
### 参数校验
@@ -127,20 +150,28 @@ return [
Tool 可以定义 UI 渲染方法,用于在终端或 ACP 客户端中展示 tool call 和 result
```typescript
```ts
import { defineTool } from '@deepseek-ai/dsh-tools'
defineTool({
name: 'bash',
// ...
description: 'Run a shell command.',
parameters: {
command: { type: 'string', required: true },
},
async execute(args) {
return [{ type: 'text', text: `ran: ${args.command}` }]
},
presentCall(args) {
return {
intent: 'terminal',
title: `bash(${JSON.stringify(args.command).slice(0, 60)})`,
card: 'terminal',
title: args.command.slice(0, 60),
}
},
presentResult(args, result) {
return {
intent: 'terminal',
body: result.content.map(b => b.type === 'text' ? b.text : '').join(''),
card: 'terminal',
output: result.content.map(b => b.type === 'text' ? b.text : '').join(''),
}
},
})
@@ -152,20 +183,32 @@ defineTool({
`ctx.tools.register()` 返回值就是 disposer。但由于你在 `ctx` 上调用,框架已经自动追踪了这个注册——插件卸载时会自动移除 tool。你不需要手动调用 disposer。
```typescript
```ts
import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
declare const ctx: Context
// 这样就够了:
ctx.tools.register(defineTool({ /* ... */ }))
ctx.tools.register(defineTool({
name: 'noop',
description: 'Do nothing.',
parameters: {},
async execute() {
return []
},
}))
// 不需要:
// const dispose = ctx.tools.register(...)
// ctx.on('dispose', dispose)
// ctx.effect(() => dispose)
```
## 完整实战示例
一个文件计数 tool
```typescript
```ts
import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { readdir } from 'node:fs/promises'

View File

@@ -6,7 +6,17 @@
### 监听事件
```typescript
```ts
import type { Context } from 'cordis'
declare module 'cordis' {
interface Events {
'event-name'(payload: string): void
}
}
declare const ctx: Context
ctx.on('event-name', (payload) => {
// 处理事件
})
@@ -14,7 +24,18 @@ ctx.on('event-name', (payload) => {
### 触发事件
```typescript
```ts
import type { Context } from 'cordis'
declare module 'cordis' {
interface Events {
'event-name'(payload: string): void
}
}
declare const ctx: Context
declare const payload: string
ctx.emit('event-name', payload)
```
@@ -26,12 +47,24 @@ Cordis 提供多种事件触发模式,适用于不同场景:
所有监听器并行执行,不关心返回值:
```typescript
```ts
import type { Context } from 'cordis'
declare module 'cordis' {
interface Events {
'my-plugin/turn-end'(agentId: string, turnIndex: number): void
}
}
declare const ctx: Context
declare const agentId: string
declare const turnIndex: number
// 触发
ctx.emit('agent/turn-end', { agentId, turnIndex })
ctx.emit('my-plugin/turn-end', agentId, turnIndex)
// 监听
ctx.on('agent/turn-end', ({ agentId, turnIndex }) => {
ctx.on('my-plugin/turn-end', (agentId, turnIndex) => {
console.log(`Turn ${turnIndex} ended`)
})
```
@@ -40,7 +73,19 @@ ctx.on('agent/turn-end', ({ agentId, turnIndex }) => {
依次调用监听器,第一个返回非 `undefined` 值的结果作为最终值:
```typescript
```ts
import type { Context } from 'cordis'
declare module 'cordis' {
interface Events {
'some-check'(input: string): string | undefined
}
}
declare const ctx: Context
declare const input: string
declare function shouldBlock(input: string): boolean
// 触发
const result = ctx.bail('some-check', input)
@@ -48,6 +93,7 @@ const result = ctx.bail('some-check', input)
ctx.on('some-check', (input) => {
if (shouldBlock(input)) return 'blocked'
// 返回 undefined 继续传递给下一个监听器
return undefined
})
```
@@ -55,24 +101,47 @@ ctx.on('some-check', (input) => {
所有监听器按注册顺序依次执行(异步安全):
```typescript
```ts
import type { Context } from 'cordis'
declare module 'cordis' {
interface Events {
'setup-phase'(context: object): Promise<void> | void
}
}
declare const ctx: Context
declare const context: object
await ctx.serial('setup-phase', context)
```
### waterfall — 管道
每个监听器接收前一个的输出,形成数据管道。**必须调用 `next()` 传递给下游**,不调用即为否决:
监听器围绕默认实现层层包裹,形成数据管道。**必须调用 `next()` 委托给下游**,不调用即为否决:
```typescript
// 触发
const finalMessages = await ctx.waterfall('llm/pre-request', messages)
```ts
import type { Context } from 'cordis'
import type { Message } from '@deepseek-ai/dsh-llm'
declare module 'cordis' {
interface Events {
'my-plugin/messages'(messages: Message[], next: () => Promise<Message[]>): Promise<Message[]>
}
}
declare const ctx: Context
declare const messages: Message[]
declare const extraMessage: Message
// 触发:最后一个参数是默认实现(所有监听器都调用 next 时的最终值)
const finalMessages = await ctx.waterfall('my-plugin/messages', messages, async () => messages)
// 监听(必须调用 next
ctx.on('llm/pre-request', async (messages, next) => {
// 可以修改 messages
messages.push(extraMessage)
// 必须调用 next() 传递给下一个监听器
return next(messages)
ctx.on('my-plugin/messages', async (messages, next) => {
// next() 委托给下游监听器(最终到达默认实现),返回值可以被加工
const result = await next()
return [...result, extraMessage]
})
```
@@ -84,11 +153,13 @@ Waterfall 监听器**必须调用 `next()`**。不调用 `next` 等于否决整
Harness 使用 TypeScript 声明合并来为事件提供类型安全:
```typescript
```ts
import type {} from 'cordis'
declare module 'cordis' {
interface Events {
'my-plugin/ready': (payload: { id: string }) => void
'my-plugin/check': (input: string) => boolean | undefined
'my-plugin/ready'(payload: { id: string }): void
'my-plugin/check'(input: string): boolean | undefined
}
}
@@ -101,24 +172,30 @@ declare module 'cordis' {
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 — 压缩结束
agent/pre-step 每个 step 开始前的检查点serial
agent/step-result — step 的 assistant 消息组装完成waterfall
tools/pre-execute — tool 执行前的允许/拒绝门waterfall
tools/post-execute — tool 执行后的检查/改写缝waterfall
llm/stream — 每次流式模型调用的环绕点waterfall
session/event — 会话事件被记录emit
session/flush — 会话持久化检查点parallel
```
完整的事件列表(含每个事件的签名与派发模式)见仓库中的 `docs/cordis-catalog/events.md`
## 事件也是效果
通过 `ctx.on()` 注册的监听器会在插件卸载时自动移除:
```typescript
```ts
import type { Context } from 'cordis'
import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent'
declare function handler(agent: Agent, status: AgentStatus): void
export function apply(ctx: Context) {
// 这个监听器在插件 dispose 时自动清理
ctx.on('agent/turn-end', handler)
ctx.on('agent/status', handler)
}
```
@@ -126,22 +203,21 @@ export function apply(ctx: Context) {
一个记录所有 tool 调用的简单插件:
```typescript
```ts
import type { Context } from 'cordis'
import type {} from '@deepseek-ai/dsh-tools'
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 }) => {
ctx.on('tools/execute', async (exec, next) => {
console.log(`[tool] ${exec.name}(${JSON.stringify(exec.arguments)})`)
const result = await next()
const text = result.content
.filter(b => b.type === 'text')
.map(b => b.text)
.map(b => b.type === 'text' ? b.text : '')
.join('')
console.log(`[tool result] ${text.slice(0, 100)}`)
return result
})
}
```

View File

@@ -25,7 +25,11 @@ ACTIVE → UNLOADING → DISPOSED
声明了 `inject` 的插件不会立即加载,而是等待依赖的服务就绪:
```typescript
```ts
import type { Context } from 'cordis'
import type {} from '@deepseek-ai/dsh-tools'
import type {} from '@deepseek-ai/dsh-llm'
export const inject = ['tools', 'llm']
export function apply(ctx: Context) {
@@ -39,10 +43,21 @@ export function apply(ctx: Context) {
通过 `ctx` 做的任何注册,在插件卸载时都会自动撤销:
```typescript
```ts
import type { Context } from 'cordis'
declare module 'cordis' {
interface Events {
'my-plugin/some-event'(): void
}
}
declare function handler(): void
declare function createConnection(): { close(): void }
export function apply(ctx: Context) {
// 事件监听——卸载时自动移除
ctx.on('some-event', handler)
ctx.on('my-plugin/some-event', handler)
// 自定义资源——卸载时调用返回的函数
ctx.effect(() => {
@@ -64,7 +79,11 @@ export function apply(ctx: Context) {
`ctx.plugin()` 创建子 Fiber它继承父上下文但有独立的生命周期
```typescript
```ts
import type { Context } from 'cordis'
declare function childPlugin(ctx: Context): void
export function apply(ctx: Context) {
// 注册一个子插件
ctx.plugin(childPlugin)
@@ -77,11 +96,16 @@ export function apply(ctx: Context) {
当你需要提前终止一个插件实例:
```typescript
```ts
import type { Context } from 'cordis'
declare const ctx: Context
declare function myPlugin(ctx: Context): void
const fiber = ctx.plugin(myPlugin)
// 之后可以手动 dispose
fiber.dispose()
await fiber.dispose()
```
`dispose` 保证:
@@ -101,18 +125,14 @@ fiber.dispose()
## 实战:理解生命周期
```typescript
`apply` 函数体就是加载钩子;卸载没有专门的事件——把清理逻辑放进 `ctx.effect()` 的返回函数即可:
```ts
import type { Context } from 'cordis'
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')
@@ -124,12 +144,10 @@ export function apply(ctx: Context) {
```
plugin loading
effect registered
context ready
```
卸载时输出(逆序)
卸载时输出:
```
plugin disposing
effect cleaned up
```

View File

@@ -6,10 +6,17 @@
在 Harness 中,`tools``llm``agents` 都是服务。服务是挂载在 `ctx` 上的命名能力:
```typescript
```ts
import type { Context } from 'cordis'
import type {} from '@deepseek-ai/dsh-tools'
import type {} from '@deepseek-ai/dsh-llm'
import type {} from '@deepseek-ai/dsh-agent'
declare const ctx: Context
ctx.tools // ToolRegistry 服务
ctx.llm // LLM 服务
ctx.agents // Agent 服务
ctx.agents // Agent 注册表服务
```
任何插件都可以提供一个新服务,供其他插件使用。
@@ -18,12 +25,22 @@ ctx.agents // Agent 服务
声明 `inject` 来使用已有服务:
```typescript
```ts
import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const inject = ['tools']
export function apply(ctx: Context) {
// ctx.tools 在这里一定存在且就绪
ctx.tools.register(/* ... */)
ctx.tools.register(defineTool({
name: 'demo',
description: 'Demo tool.',
parameters: {},
async execute() {
return []
},
}))
}
```
@@ -33,8 +50,9 @@ export function apply(ctx: Context) {
### 使用 Service 基类
```typescript
```ts
import { Service, type Context } from 'cordis'
import type {} from '@deepseek-ai/dsh-llm'
export default class MetricsService extends Service {
static inject = ['llm'] // 本服务也可以依赖其他服务
@@ -52,7 +70,9 @@ export default class MetricsService extends Service {
加载这个插件后,其他插件就可以通过 `ctx.metrics` 访问它:
```typescript
```ts
import type { Context } from 'cordis'
export const inject = ['metrics']
export function apply(ctx: Context) {
@@ -64,7 +84,7 @@ export function apply(ctx: Context) {
使用 TypeScript 声明合并让 `ctx.metrics` 有正确类型:
```typescript
```ts
import { Service, type Context } from 'cordis'
declare module 'cordis' {
@@ -84,14 +104,21 @@ export default class MetricsService extends Service {
## 依赖的行为
### 必选依赖 vs 可选依赖
### 必选依赖 vs 可选读取
`inject` 声明的依赖都是必选的:服务不存在时,插件不会加载。如果只想"有则用之",用 `ctx.get()` 读取——服务不存在时返回 `undefined`,插件照常加载:
```ts
import type { Context } from 'cordis'
```typescript
// 必选:服务不存在时,插件不会加载
export const inject = ['tools']
// 可选:服务不存在时,插件仍然加载,但 ctx.xxx 可能是 undefined
export const inject = { optional: ['metrics'] }
export function apply(ctx: Context) {
// 可选读取:不声明 inject服务不存在时返回 undefined
const metrics = ctx.get('metrics')
metrics?.record('plugin_loaded', 1)
}
```
### 服务消失时的行为
@@ -133,13 +160,14 @@ export const inject = { optional: ['metrics'] }
|--------|--------|------|
| `tools` | dsh-tools | Tool 注册表 |
| `llm` | dsh-llm | LLM 调用 + 适配器注册 |
| `agents` | dsh-agent | Agent 实例管理 |
| `session` | dsh-session | 会话事件流 |
| `agents` | dsh-agent | Agent 注册表 |
| `agentLoop` | dsh-agent-loop | Agent 创建与循环执行 |
| `sessions` | dsh-session | 会话存储与事件流 |
| `systemPrompt` | dsh-system-prompt | 系统提示词组装 |
| `bash` | dsh-bash-local | Bash 命令执行 |
| `fs` | dsh-fs-local | 文件系统操作 |
| `subagent` | dsh-subagent | 子代理委派 |
| `persistence` | dsh-session-persistence | 会话持久化 |
| `bash` | dsh-bash实现dsh-bash-local | Bash 命令执行 |
| `fs` | dsh-fs实现dsh-fs-local | 文件系统操作 |
| `subagents` | dsh-subagent | 子代理委派 |
| `sessionPersistence` | dsh-session-persistence(实现:-jsonl / -sqlite | 会话持久化 |
## 下一步

View File

@@ -64,7 +64,7 @@
### 第一步:定义接口
```typescript
```ts
// packages/my-cap/my-cap/src/index.ts
import { Service, type Context } from 'cordis'
@@ -94,7 +94,7 @@ 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'
@@ -115,7 +115,7 @@ export function apply(ctx: Context) {
### 第三步:编写消费者 (tool)
```typescript
```ts
// packages/my-cap/tool-my-cap/src/index.ts
import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

View File

@@ -8,7 +8,7 @@ LLM 适配器是一个继承 `LlmAdapter` 的类,实现 `stream()` 方法,
## 最小实现
```typescript
```ts
import type { Context } from 'cordis'
import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'
@@ -45,47 +45,51 @@ export function apply(ctx: Context, config: Config) {
`stream()` 必须按以下协议 yield chunk
```typescript
// 1. 每个内容块以 block-start 开始
yield { type: 'block-start', index: 0, blockType: 'text' }
```ts
import { CallId, type StreamChunk } from '@deepseek-ai/dsh-llm'
// 2. 文本块使用 text-delta
yield { type: 'text-delta', index: 0, text: 'Hello' }
yield { type: 'text-delta', index: 0, text: ' world' }
async function* demo(): AsyncIterable<StreamChunk> {
// 1. 每个内容块以 block-start 开始
yield { type: 'block-start', index: 0, blockType: 'text' }
// 3. 每个内容块以 block-end 结束(携带完整 block
yield {
type: 'block-end',
index: 0,
block: { type: 'text', text: 'Hello world' },
}
// 2. 文本块使用 text-delta
yield { type: 'text-delta', index: 0, text: 'Hello' }
yield { type: 'text-delta', index: 0, text: ' 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',
// 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',
arguments: '{"command":"ls"}',
},
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
}
// 5. Token 用量
yield { type: 'usage', usage: { inputTokens: 100, outputTokens: 50 } }
// 6. 结束原因
yield { type: 'finish', reason: { kind: 'stop' } }
// 或: { kind: 'tool-calls' } 表示模型想调用 tool
```
### 关键规则
@@ -100,28 +104,31 @@ yield { type: 'finish', reason: { kind: 'stop' } }
`stream()` 接收的请求包含:
```typescript
interface GenerateOptions {
/** 模型名 */
model: string
/** 对话历史 */
messages: Message[]
/** 可用的 tool 列表 */
tools?: ToolSpec[]
/** 系统提示词 */
system?: string
/** 最大输出 token */
maxTokens?: number
/** 温度 */
temperature?: number
}
```ts
import type { GenerateOptions } from '@deepseek-ai/dsh-llm'
declare const options: GenerateOptions
options.model // 模型名
options.messages // 对话历史 (Message[])
options.tools // 可用的 tool schema 列表 (ToolSchema[])
options.system // 系统提示词
options.maxTokens // 最大输出 token
options.temperature // 温度
options.signal // 取消信号(必须响应)
```
你的适配器需要将这些映射到具体 API 的参数。
## 注册适配器
```typescript
```ts
import type { Context } from 'cordis'
import type { LlmAdapter } from '@deepseek-ai/dsh-llm'
declare const ctx: Context
declare const adapter: LlmAdapter
ctx.llm.registerAdapter(['model-name-1', 'model-name-2'], adapter)
```
@@ -158,12 +165,18 @@ mock 适配器是学习 StreamChunk 协议的最佳起点——它用纯本地
适配器中的异常会被 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}`)
```ts
import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'
class HttpAdapter extends LlmAdapter {
private endpoint = 'https://api.example.com/v1/chat'
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
const response = await fetch(this.endpoint, { method: 'POST' })
if (!response.ok) {
throw new Error(`API error: ${response.status}`)
}
// ... 正常流式处理
}
// ... 正常流式处理
}
```

View File

@@ -87,6 +87,7 @@ Harness 使用 `cordis.yml` 描述一个 Agent 加载哪些插件、以什么参
# 自动压缩:对话太长时自动总结旧内容,腾出上下文空间
# contextWindow 是模型能看到的 token 上限
# thresholdRatio 超过这个比例就触发压缩
# compactionRetries 是压缩后仍超标时的额外重试次数
- id: compact-basic
name: '@deepseek-ai/dsh-compact-basic'
config:
@@ -94,6 +95,7 @@ Harness 使用 `cordis.yml` 描述一个 Agent 加载哪些插件、以什么参
thresholdRatio: 0.8
retainTokens: 20480
maxTokens: 8192
compactionRetries: 1
# 子代理:把子任务分配给独立的 Agent 去做
# subagent 是服务注册spawn/fork 是两种委派方式:
@@ -125,6 +127,16 @@ Harness 使用 `cordis.yml` 描述一个 Agent 加载哪些插件、以什么参
provider: fork
toolName: subagent_fork
# 动态工作流:模型编写一段编排脚本,引擎在独立 worker 线程里运行它,
# 并通过上面的 spawn 后端把 agent() 调用分发为子代理
- id: workflow-workerthread
name: '@deepseek-ai/dsh-workflow-workerthread'
config:
provider: spawn
- id: tool-workflow
name: '@deepseek-ai/dsh-tool-workflow'
# 任务追踪:模型可以用 todo_write 记录和更新任务清单
- id: tool-todo
name: '@deepseek-ai/dsh-tool-todo'
@@ -156,9 +168,13 @@ Harness 使用 `cordis.yml` 描述一个 Agent 加载哪些插件、以什么参
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `name` | string | 是 | 插件来源npm 包名或相对路径) |
| `id` | string | 否 | 实例标识符,用于日志和调试 |
| `id` | string | 否 | 实例标识符,用于日志和调试。省略时由 loader 生成并写回 |
| `config` | object | 否 | 传递给插件的配置 |
| `disabled` | boolean | 否 | 设为 `true` 临时禁用该插件 |
| `group` | boolean | 否 | 标记该条目为嵌套分组(`config` 为子条目列表) |
| `inject` | array \| object | 否 | 声明该插件依赖的服务 |
| `intercept` | object | 否 | 按服务名拦截并覆盖下游配置 |
| `isolate` | object | 否 | 服务隔离:服务名 → `true` 或隔离标签 |
### 插件来源 (`name`)