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) 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) docs/ architecture, generated catalogs, RFCs, postmortems, cookbook (see docs/AGENTS.md)
scripts/ repo gates and generators 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). 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 build # tsc emits lib/types, tsdown bundles runtime
pnpm run hygiene # knip + publint + workspace constraints + NodeNext consumer check 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 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:echo # mock-model REPL, no key needed
pnpm run demo:repl # real REPL coding agent (needs DEEPSEEK_API_KEY) 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) 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:coverage
pnpm run test:snapshot pnpm run test:snapshot
pnpm run doc-sync pnpm run doc-sync
pnpm run website:build
pnpm run verify-module-graph pnpm run verify-module-graph
pnpm run build pnpm run build
pnpm run hygiene pnpm run hygiene

View File

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

View File

@@ -9,7 +9,8 @@
}, },
"workspaces": [ "workspaces": [
"vendor/*", "vendor/*",
"packages/*/*" "packages/*/*",
"website"
], ],
"scripts": { "scripts": {
"build": "tsc -b tsconfig.build.json && tsdown", "build": "tsc -b tsconfig.build.json && tsdown",
@@ -60,6 +61,8 @@
"verify-persistence-catalog": "tsx scripts/gen-persistence-catalog.ts --check", "verify-persistence-catalog": "tsx scripts/gen-persistence-catalog.ts --check",
"gen-module-graph": "tsx scripts/gen-module-graph.ts", "gen-module-graph": "tsx scripts/gen-module-graph.ts",
"verify-module-graph": "tsx scripts/gen-module-graph.ts --check", "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", "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", "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", "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", "@agentclientprotocol/sdk": "0.25.1",
"@stylistic/eslint-plugin": "^5.10.0", "@stylistic/eslint-plugin": "^5.10.0",
"@types/jsdom": "^28.0.3", "@types/jsdom": "^28.0.3",
"@types/js-yaml": "^4.0.9",
"@types/mdast": "^4.0.4", "@types/mdast": "^4.0.4",
"@types/node": "^22.20.0", "@types/node": "^22.20.0",
"@vitest/coverage-v8": "^4.1.8", "@vitest/coverage-v8": "^4.1.8",
"eslint": "^10.4.1", "eslint": "^10.4.1",
"fast-check": "^4.8.0", "fast-check": "^4.8.0",
"jsdom": "29.1.1", "jsdom": "29.1.1",
"js-yaml": "^4.1.0",
"knip": "^6.16.1", "knip": "^6.16.1",
"lefthook": "^2.1.9", "lefthook": "^2.1.9",
"mdast-util-from-markdown": "^2.0.3", "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: packages:
- vendor/* - vendor/*
- packages/*/* - packages/*/*
- website
peerDependencyRules: peerDependencyRules:
allowedVersions: allowedVersions:

View File

@@ -2,7 +2,8 @@
* Doc-sync gate (doc-sync-enforcement RFC, part 1): typecheck the fenced `ts` code blocks in our * 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. * 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 * extracted to a temp typecheck project and compiled against the workspace
* sources through the same project-reference boundaries used by repo * sources through the same project-reference boundaries used by repo
* typecheck. A block that is a deliberate sketch rather than compilable code * 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[] = [] const files: string[] = []
for (const pattern of markdownGlobs) { for (const pattern of markdownGlobs) {

View File

@@ -167,6 +167,7 @@ function ciPrimaryGates(): Gate[] {
...docSyncLeafGates(), ...docSyncLeafGates(),
pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }), pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }),
pnpmScript('knip', 'knip'), pnpmScript('knip', 'knip'),
pnpmScript('website-build', 'website:build', { label: 'website build' }),
pnpmScript('build', 'build', { needs: ['typecheck'] }), pnpmScript('build', 'build', { needs: ['typecheck'] }),
pnpmScript('publint', 'publint', { needs: ['build'] }), pnpmScript('publint', 'publint', { needs: ['build'] }),
pnpmScript('node-next-types', 'verify-node-next-types', { pnpmScript('node-next-types', 'verify-node-next-types', {
@@ -184,6 +185,7 @@ function ciStaticGates(): Gate[] {
...docSyncLeafGates(), ...docSyncLeafGates(),
pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }), pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }),
pnpmScript('knip', 'knip'), 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 * added to a doc with NO manifest entry is still discovered here and reported as
* an orphan, instead of being silently skipped. * 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. */ /** One manifest entry: a documented type-equiv block and its source symbol. */
interface ManifestEntry { interface ManifestEntry {

View File

@@ -55,16 +55,21 @@ Cordis 同时解决了上述两个问题:
DeepSeek Harness 将 Cordis 的可组合性应用到 Agent 开发领域: 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 插件天然是可逆的 // 一个 Harness 插件天然是可逆的
export const inject = ['tools', 'llm'] // 空间可组合:声明依赖 export const inject = ['tools', 'llm'] // 空间可组合:声明依赖
export function apply(ctx: Context) { export function apply(ctx: Context) {
// 时间可组合:注册会被自动追踪和回收 // 时间可组合:注册会被自动追踪和回收
ctx.tools.register(defineTool('my-tool', { ctx.tools.register(defineTool({
name: 'my-tool',
description: '...', description: '...',
parameters: { /* ... */ }, 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 这个 "资源") // 提供服务 = 一个 effect(占用 ctx.llm 这个 "资源")
class LlmService extends Service { class LlmService extends Service {
constructor(ctx: Context) {
super(ctx, 'llm')
}
// 当此插件卸载时,ctx.llm 被回收(effect 的逆操作) // 当此插件卸载时,ctx.llm 被回收(effect 的逆操作)
// 所有依赖 llm 的插件因 coeffect 不满足而挂起 // 所有依赖 llm 的插件因 coeffect 不满足而挂起
} }
@@ -66,7 +71,16 @@ class LlmService extends Service {
框架将领域中的所有方法都封装为 effect 版本。开发者只需调用 `ctx` 上的方法,就能自动获得时间/空间可组合性: 框架将领域中的所有方法都封装为 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) { export function apply(ctx: Context) {
// 以下每一行都是 effect——卸载时自动逆序回收 // 以下每一行都是 effect——卸载时自动逆序回收
ctx.on('agent/step-result', validateResult) ctx.on('agent/step-result', validateResult)
@@ -82,7 +96,16 @@ export function apply(ctx: Context) {
可以逐步将现有框架中的 API 替换为可组合版本,无需一次性重写: 可以逐步将现有框架中的 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 包装遗留 API
ctx.effect(() => { ctx.effect(() => {
const legacy = legacySystem.register(handler) const legacy = legacySystem.register(handler)

View File

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

View File

@@ -103,7 +103,20 @@ $$
| $\text{restore}$ | `fiber.dispose()` | 执行 Fiber 的整个回收链 | | $\text{restore}$ | `fiber.dispose()` | 执行 Fiber 的整个回收链 |
| $f^{-1}$ | dispose 返回值 / cleanup 函数 | 逆操作 | | $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) { export function apply(ctx: Context) {
// effect: 创建资源,返回其逆操作 // effect: 创建资源,返回其逆操作
ctx.effect(() => { ctx.effect(() => {
@@ -112,7 +125,7 @@ export function apply(ctx: Context) {
}) })
// 框架 API 内部已封装 effect // 框架 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) ctx.tools.register(myTool) // 内部: effect(addTool, removeTool)
} }
// 当此插件被卸载时,restore 自动按逆序执行所有 f⁻¹ // 当此插件被卸载时,restore 自动按逆序执行所有 f⁻¹

View File

@@ -4,27 +4,21 @@
## 定义 Config 类型 ## 定义 Config 类型
在插件中导出一个 `Config` 类型和可选的默认值: 在插件中导出一个 `Config` 类型,`apply` 的第二个参数就是用户配置:
```typescript ```ts
import type { Context } from 'cordis' import type { Context } from 'cordis'
export const name = 'my-plugin' export const name = 'my-plugin'
export interface Config { export interface Config {
greeting: string greeting?: string
maxRetries: number maxRetries?: number
verbose?: boolean verbose?: boolean
} }
export const Config = {
greeting: 'Hello',
maxRetries: 3,
verbose: false,
}
export function apply(ctx: Context, config: Config) { 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 maxRetries: 5
``` ```
未提供的字段使用导出的 `Config` 对象中的默认值。 只导出类型时,配置原样传入,默认值由代码自己兜底(如上面的 `??`)。想让框架代管默认值和校验,导出一个 schema(见下节)。
## Schema 校验 ## Schema 校验
对于需要严格校验的场景,使用 Schemastery 定义 schema: 对于需要默认值和严格校验的场景,额外导出一个 Schemastery schema(仓库约定以 `z` 引入)。加载时框架先用它校验并填充默认值,再把结果传给 `apply`:
```typescript ```ts
import type { Context } from 'cordis' import type { Context } from 'cordis'
import Schema from 'schemastery' import z from 'schemastery'
export const name = 'validated-plugin' export const name = 'validated-plugin'
export interface Config { export interface Config {
apiKey: string apiKey: string
timeout: number timeout?: number
mode: 'fast' | 'accurate' mode?: 'fast' | 'accurate'
} }
export const Config = Schema.object({ export const Config: z<Config> = z.object({
apiKey: Schema.string().required(), apiKey: z.string().required(),
timeout: Schema.number().default(30000), timeout: z.number().default(30000),
mode: Schema.union(['fast', 'accurate']).default('fast'), mode: z.union(['fast', 'accurate'] as const).default('fast'),
}) })
export function apply(ctx: Context, config: Config) { export function apply(ctx: Context, config: Config) {
// config 已经过校验,类型安全 // config 已经过校验,类型安全,默认值已填充
} }
``` ```
@@ -74,13 +68,14 @@ Schema 在插件加载时执行校验。如果配置不合法,插件会加载
Harness 的约定:**任何两个部署可能想要不同值的东西,都应该是配置字段**。 Harness 的约定:**任何两个部署可能想要不同值的东西,都应该是配置字段**。
```typescript ```ts
// 错误 — 硬编码超时时间 // 错误 — 硬编码超时时间
const TIMEOUT = 30000 const TIMEOUT = 30000
// 正确 — 可配置 // 正确 — 可配置
export interface Config { 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) { 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`) 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` 注册能力: 在 Harness 中,插件是一个导出 `apply` 函数的 TypeScript 模块。框架在加载时调用 `apply`,传入一个 `ctx`(上下文对象),你通过 `ctx` 注册能力:
```typescript ```ts
import type { Context } from 'cordis' import type { Context } from 'cordis'
export const name = 'my-plugin' export const name = 'my-plugin'
@@ -22,16 +22,14 @@ export function apply(ctx: Context) {
在你的项目目录下创建 `src/my-plugin.ts`: 在你的项目目录下创建 `src/my-plugin.ts`:
```typescript ```ts
import type { Context } from 'cordis' import type { Context } from 'cordis'
export const name = 'hello-plugin' export const name = 'hello-plugin'
export function apply(ctx: Context) { export function apply(ctx: Context) {
// 监听 agent-loop 的 ready 事件 // apply 函数体在插件加载时执行
ctx.on('ready', () => { console.log('[hello-plugin] 插件已加载!')
console.log('[hello-plugin] 插件已加载!')
})
} }
``` ```
@@ -52,7 +50,9 @@ export function apply(ctx: Context) {
如果你有需要手动清理的资源(比如一个网络连接),用 `ctx.effect()` 告诉框架怎么清理: 如果你有需要手动清理的资源(比如一个网络连接),用 `ctx.effect()` 告诉框架怎么清理:
```typescript ```ts
import type { Context } from 'cordis'
export function apply(ctx: Context) { export function apply(ctx: Context) {
ctx.effect(() => { ctx.effect(() => {
const timer = setInterval(() => { const timer = setInterval(() => {
@@ -69,13 +69,23 @@ export function apply(ctx: Context) {
如果你的插件需要使用其他服务(如 `tools`、`llm`),需要声明 `inject`: 如果你的插件需要使用其他服务(如 `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 name = 'my-tool-plugin'
export const inject = ['tools'] export const inject = ['tools']
export function apply(ctx: Context) { export function apply(ctx: Context) {
// ctx.tools 现在可用 // 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 { export default {
name: 'my-plugin', name: 'my-plugin',
inject: ['tools'], inject: ['tools'],
@@ -99,8 +112,9 @@ export default {
### 类形式 ### 类形式
```typescript ```ts
import { Service } from 'cordis' import { Service, type Context } from 'cordis'
import type {} from '@deepseek-ai/dsh-tools'
export default class MyService extends Service { export default class MyService extends Service {
static inject = ['tools'] static inject = ['tools']
@@ -109,8 +123,9 @@ export default class MyService extends Service {
super(ctx, 'myService') 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 的插件: 参考仓库中的 `examples/echo-agent/src/echo-tool.ts`,这是一个注册 tool 的插件:
```typescript ```ts
import type { Context } from 'cordis' import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools' import { defineTool } from '@deepseek-ai/dsh-tools'

View File

@@ -4,7 +4,7 @@ Tool 是模型可以调用的能力。本文介绍如何用 `defineTool` 编写
## 最小示例 ## 最小示例
```typescript ```ts
import type { Context } from 'cordis' import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools' import { defineTool } from '@deepseek-ai/dsh-tools'
@@ -32,28 +32,34 @@ export function apply(ctx: Context) {
### 基本类型 ### 基本类型
```typescript ```ts
parameters: { import type { SchemaSpec } from '@deepseek-ai/dsh-tools'
const parameters = {
path: { type: 'string', required: true }, path: { type: 'string', required: true },
limit: { type: 'number' }, limit: { type: 'number' },
recursive: { type: 'boolean' }, recursive: { type: 'boolean' },
} } satisfies SchemaSpec
// 推导类型: { path: string; limit?: number; recursive?: boolean } // 推导类型: { path: string; limit?: number; recursive?: boolean }
``` ```
### 枚举 ### 枚举
```typescript ```ts
parameters: { import type { SchemaSpec } from '@deepseek-ai/dsh-tools'
const parameters = {
mode: { type: 'string', required: true, enum: ['read', 'write', 'append'] }, mode: { type: 'string', required: true, enum: ['read', 'write', 'append'] },
} } satisfies SchemaSpec
// 推导类型: { mode: string } (运行时校验 enum 值) // 推导类型: { mode: string } (运行时校验 enum 值)
``` ```
### 嵌套对象 ### 嵌套对象
```typescript ```ts
parameters: { import type { SchemaSpec } from '@deepseek-ai/dsh-tools'
const parameters = {
options: { options: {
type: 'object', type: 'object',
properties: { properties: {
@@ -61,19 +67,21 @@ parameters: {
retries: { type: 'number' }, retries: { type: 'number' },
}, },
}, },
} } satisfies SchemaSpec
// 推导类型: { options?: { timeout?: number; retries?: number } } // 推导类型: { options?: { timeout?: number; retries?: number } }
``` ```
### 数组 ### 数组
```typescript ```ts
parameters: { import type { SchemaSpec } from '@deepseek-ai/dsh-tools'
const parameters = {
tags: { tags: {
type: 'array', type: 'array',
items: { type: 'string' }, items: { type: 'string' },
}, },
} } satisfies SchemaSpec
// 推导类型: { tags?: string[] } // 推导类型: { tags?: string[] }
``` ```
@@ -92,29 +100,44 @@ parameters: {
`execute` 接收经过校验的 `args`(类型自动推导)和一个 `exec` 上下文对象: `execute` 接收经过校验的 `args`(类型自动推导)和一个 `exec` 上下文对象:
```typescript ```ts
async execute(args, exec) { import { defineTool } from '@deepseek-ai/dsh-tools'
// args: 根据 parameters 自动推导的类型
// exec: ToolExecution 对象,提供执行上下文
// 返回 ContentBlock 数组 defineTool({
return [{ type: 'text', text: 'result here' }] name: 'demo',
} description: 'Demo tool.',
parameters: {},
async execute(args, exec) {
// args: 根据 parameters 自动推导的类型
// exec: ToolExecution 对象,提供执行上下文
// 返回 ContentBlock 数组
return [{ type: 'text', text: 'result here' }]
},
})
``` ```
### 返回值 ### 返回值
`execute` 必须返回一个 `ContentBlock[]`,告诉模型 tool 的执行结果: `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 // 多个 block
return [ function multiBlockResult(): ContentBlock[] {
{ type: 'text', text: 'Found 3 matches:' }, return [
{ type: 'text', text: matchResults.join('\n') }, { type: 'text', text: 'Found 3 matches:' },
] { type: 'text', text: matchResults.join('\n') },
]
}
``` ```
### 参数校验 ### 参数校验
@@ -127,20 +150,28 @@ return [
Tool 可以定义 UI 渲染方法,用于在终端或 ACP 客户端中展示 tool call 和 result: Tool 可以定义 UI 渲染方法,用于在终端或 ACP 客户端中展示 tool call 和 result:
```typescript ```ts
import { defineTool } from '@deepseek-ai/dsh-tools'
defineTool({ defineTool({
name: 'bash', 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) { presentCall(args) {
return { return {
intent: 'terminal', card: 'terminal',
title: `bash(${JSON.stringify(args.command).slice(0, 60)})`, title: args.command.slice(0, 60),
} }
}, },
presentResult(args, result) { presentResult(args, result) {
return { return {
intent: 'terminal', card: 'terminal',
body: result.content.map(b => b.type === 'text' ? b.text : '').join(''), output: result.content.map(b => b.type === 'text' ? b.text : '').join(''),
} }
}, },
}) })
@@ -152,20 +183,32 @@ defineTool({
`ctx.tools.register()` 返回值就是 disposer。但由于你在 `ctx` 上调用,框架已经自动追踪了这个注册——插件卸载时会自动移除 tool。你不需要手动调用 disposer。 `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(...) // const dispose = ctx.tools.register(...)
// ctx.on('dispose', dispose) // ctx.effect(() => dispose)
``` ```
## 完整实战示例 ## 完整实战示例
一个文件计数 tool: 一个文件计数 tool:
```typescript ```ts
import type { Context } from 'cordis' import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools' import { defineTool } from '@deepseek-ai/dsh-tools'
import { readdir } from 'node:fs/promises' 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) => { 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) 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`) console.log(`Turn ${turnIndex} ended`)
}) })
``` ```
@@ -40,7 +73,19 @@ ctx.on('agent/turn-end', ({ agentId, turnIndex }) => {
依次调用监听器,第一个返回非 `undefined` 值的结果作为最终值: 依次调用监听器,第一个返回非 `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) const result = ctx.bail('some-check', input)
@@ -48,6 +93,7 @@ const result = ctx.bail('some-check', input)
ctx.on('some-check', (input) => { ctx.on('some-check', (input) => {
if (shouldBlock(input)) return 'blocked' if (shouldBlock(input)) return 'blocked'
// 返回 undefined 继续传递给下一个监听器 // 返回 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) await ctx.serial('setup-phase', context)
``` ```
### waterfall — 管道 ### waterfall — 管道
每个监听器接收前一个的输出,形成数据管道。**必须调用 `next()` 传递给下游**,不调用即为否决: 监听器围绕默认实现层层包裹,形成数据管道。**必须调用 `next()` 委托给下游**,不调用即为否决:
```typescript ```ts
// 触发 import type { Context } from 'cordis'
const finalMessages = await ctx.waterfall('llm/pre-request', messages) 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) // 监听(必须调用 next)
ctx.on('llm/pre-request', async (messages, next) => { ctx.on('my-plugin/messages', async (messages, next) => {
// 可以修改 messages // next() 委托给下游监听器(最终到达默认实现),返回值可以被加工
messages.push(extraMessage) const result = await next()
// 必须调用 next() 传递给下一个监听器 return [...result, extraMessage]
return next(messages)
}) })
``` ```
@@ -84,11 +153,13 @@ Waterfall 监听器**必须调用 `next()`**。不调用 `next` 等于否决整
Harness 使用 TypeScript 声明合并来为事件提供类型安全: Harness 使用 TypeScript 声明合并来为事件提供类型安全:
```typescript ```ts
import type {} from 'cordis'
declare module 'cordis' { declare module 'cordis' {
interface Events { interface Events {
'my-plugin/ready': (payload: { id: string }) => void 'my-plugin/ready'(payload: { id: string }): void
'my-plugin/check': (input: string) => boolean | undefined 'my-plugin/check'(input: string): boolean | undefined
} }
} }
@@ -101,24 +172,30 @@ declare module 'cordis' {
Harness 事件遵循 `namespace/action` 命名: Harness 事件遵循 `namespace/action` 命名:
``` ```
agent/pre-step — agent 执行一步之前 agent/pre-step — 每个 step 开始前的检查点(serial)
agent/post-step — agent 执行一步之后 agent/step-result — step 的 assistant 消息组装完成(waterfall)
tool/call — tool 被调用 tools/pre-execute — tool 执行前的允许/拒绝门(waterfall)
tool/result — tool 返回结果 tools/post-execute — tool 执行后的检查/改写缝(waterfall)
llm/pre-request — LLM 请求发送前 llm/stream — 每次流式模型调用的环绕点(waterfall)
session/event — 会话事件被记录 session/event — 会话事件被记录(emit)
compact/start — 压缩开始 session/flush — 会话持久化检查点(parallel)
compact/end — 压缩结束
``` ```
完整的事件列表(含每个事件的签名与派发模式)见仓库中的 `docs/cordis-catalog/events.md`。
## 事件也是效果 ## 事件也是效果
通过 `ctx.on()` 注册的监听器会在插件卸载时自动移除: 通过 `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) { export function apply(ctx: Context) {
// 这个监听器在插件 dispose 时自动清理 // 这个监听器在插件 dispose 时自动清理
ctx.on('agent/turn-end', handler) ctx.on('agent/status', handler)
} }
``` ```
@@ -126,22 +203,21 @@ export function apply(ctx: Context) {
一个记录所有 tool 调用的简单插件: 一个记录所有 tool 调用的简单插件:
```typescript ```ts
import type { Context } from 'cordis' import type { Context } from 'cordis'
import type {} from '@deepseek-ai/dsh-tools'
export const name = 'tool-logger' export const name = 'tool-logger'
export function apply(ctx: Context) { export function apply(ctx: Context) {
ctx.on('tool/call', ({ name, args }) => { ctx.on('tools/execute', async (exec, next) => {
console.log(`[tool] ${name}(${JSON.stringify(args)})`) console.log(`[tool] ${exec.name}(${JSON.stringify(exec.arguments)})`)
}) const result = await next()
ctx.on('tool/result', ({ name, result }) => {
const text = result.content const text = result.content
.filter(b => b.type === 'text') .map(b => b.type === 'text' ? b.text : '')
.map(b => b.text)
.join('') .join('')
console.log(`[tool result] ${text.slice(0, 100)}`) console.log(`[tool result] ${text.slice(0, 100)}`)
return result
}) })
} }
``` ```

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ LLM 适配器是一个继承 `LlmAdapter` 的类,实现 `stream()` 方法,
## 最小实现 ## 最小实现
```typescript ```ts
import type { Context } from 'cordis' import type { Context } from 'cordis'
import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm' 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: `stream()` 必须按以下协议 yield chunk:
```typescript ```ts
// 1. 每个内容块以 block-start 开始 import { CallId, type StreamChunk } from '@deepseek-ai/dsh-llm'
yield { type: 'block-start', index: 0, blockType: 'text' }
// 2. 文本块使用 text-delta async function* demo(): AsyncIterable<StreamChunk> {
yield { type: 'text-delta', index: 0, text: 'Hello' } // 1. 每个内容块以 block-start 开始
yield { type: 'text-delta', index: 0, text: ' world' } yield { type: 'block-start', index: 0, blockType: 'text' }
// 3. 每个内容块以 block-end 结束(携带完整 block) // 2. 文本块使用 text-delta
yield { yield { type: 'text-delta', index: 0, text: 'Hello' }
type: 'block-end', yield { type: 'text-delta', index: 0, text: ' world' }
index: 0,
block: { type: 'text', text: 'Hello world' },
}
// 4. Tool call 块 // 3. 每个内容块以 block-end 结束(携带完整 block)
yield { type: 'block-start', index: 1, blockType: 'tool-call' } yield {
yield { type: 'block-end',
type: 'tool-call-delta', index: 0,
index: 1, block: { type: 'text', text: 'Hello world' },
id: CallId('call-123'), }
name: 'bash',
argumentsDelta: '{"command":"ls"}', // 4. Tool call 块
} yield { type: 'block-start', index: 1, blockType: 'tool-call' }
yield { yield {
type: 'block-end', type: 'tool-call-delta',
index: 1, index: 1,
block: {
type: 'tool-call',
id: CallId('call-123'), id: CallId('call-123'),
name: 'bash', 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()` 接收的请求包含: `stream()` 接收的请求包含:
```typescript ```ts
interface GenerateOptions { import type { GenerateOptions } from '@deepseek-ai/dsh-llm'
/** 模型名 */
model: string declare const options: GenerateOptions
/** 对话历史 */
messages: Message[] options.model // 模型名
/** 可用的 tool 列表 */ options.messages // 对话历史 (Message[])
tools?: ToolSpec[] options.tools // 可用的 tool schema 列表 (ToolSchema[])
/** 系统提示词 */ options.system // 系统提示词
system?: string options.maxTokens // 最大输出 token
/** 最大输出 token */ options.temperature // 温度
maxTokens?: number options.signal // 取消信号(必须响应)
/** 温度 */
temperature?: number
}
``` ```
你的适配器需要将这些映射到具体 API 的参数。 你的适配器需要将这些映射到具体 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) ctx.llm.registerAdapter(['model-name-1', 'model-name-2'], adapter)
``` ```
@@ -158,12 +165,18 @@ mock 适配器是学习 StreamChunk 协议的最佳起点——它用纯本地
适配器中的异常会被 agent-loop 捕获并转化为 `LlmError`,告知上层。不需要在 `stream()` 内部做错误恢复——让异常冒泡即可。 适配器中的异常会被 agent-loop 捕获并转化为 `LlmError`,告知上层。不需要在 `stream()` 内部做错误恢复——让异常冒泡即可。
```typescript ```ts
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> { import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'
const response = await fetch(this.endpoint, { /* ... */ })
if (!response.ok) { class HttpAdapter extends LlmAdapter {
throw new Error(`API error: ${response.status}`) 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 上限 # contextWindow 是模型能看到的 token 上限
# thresholdRatio 超过这个比例就触发压缩 # thresholdRatio 超过这个比例就触发压缩
# compactionRetries 是压缩后仍超标时的额外重试次数
- id: compact-basic - id: compact-basic
name: '@deepseek-ai/dsh-compact-basic' name: '@deepseek-ai/dsh-compact-basic'
config: config:
@@ -94,6 +95,7 @@ Harness 使用 `cordis.yml` 描述一个 Agent 加载哪些插件、以什么参
thresholdRatio: 0.8 thresholdRatio: 0.8
retainTokens: 20480 retainTokens: 20480
maxTokens: 8192 maxTokens: 8192
compactionRetries: 1
# 子代理:把子任务分配给独立的 Agent 去做 # 子代理:把子任务分配给独立的 Agent 去做
# subagent 是服务注册,spawn/fork 是两种委派方式: # subagent 是服务注册,spawn/fork 是两种委派方式:
@@ -125,6 +127,16 @@ Harness 使用 `cordis.yml` 描述一个 Agent 加载哪些插件、以什么参
provider: fork provider: fork
toolName: subagent_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 记录和更新任务清单 # 任务追踪:模型可以用 todo_write 记录和更新任务清单
- id: tool-todo - id: tool-todo
name: '@deepseek-ai/dsh-tool-todo' name: '@deepseek-ai/dsh-tool-todo'
@@ -156,9 +168,13 @@ Harness 使用 `cordis.yml` 描述一个 Agent 加载哪些插件、以什么参
| 字段 | 类型 | 必填 | 说明 | | 字段 | 类型 | 必填 | 说明 |
|------|------|------|------| |------|------|------|------|
| `name` | string | 是 | 插件来源(npm 包名或相对路径) | | `name` | string | 是 | 插件来源(npm 包名或相对路径) |
| `id` | string | 否 | 实例标识符,用于日志和调试 | | `id` | string | 否 | 实例标识符,用于日志和调试。省略时由 loader 生成并写回 |
| `config` | object | 否 | 传递给插件的配置 | | `config` | object | 否 | 传递给插件的配置 |
| `disabled` | boolean | 否 | 设为 `true` 临时禁用该插件 | | `disabled` | boolean | 否 | 设为 `true` 临时禁用该插件 |
| `group` | boolean | 否 | 标记该条目为嵌套分组(`config` 为子条目列表) |
| `inject` | array \| object | 否 | 声明该插件依赖的服务 |
| `intercept` | object | 否 | 按服务名拦截并覆盖下游配置 |
| `isolate` | object | 否 | 服务隔离:服务名 → `true` 或隔离标签 |
### 插件来源 (`name`) ### 插件来源 (`name`)