diff --git a/website/.gitignore b/website/.gitignore new file mode 100644 index 0000000000..2c1fa99cb4 --- /dev/null +++ b/website/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.vitepress/dist/ +.vitepress/cache/ diff --git a/website/.vitepress/config/index.ts b/website/.vitepress/config/index.ts new file mode 100644 index 0000000000..b4978ca4aa --- /dev/null +++ b/website/.vitepress/config/index.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'vitepress' +import { zhCN } from './zh-CN' + +export default defineConfig({ + title: 'DeepSeek Harness', + description: '插件化 Agent 开发框架', + + locales: { + 'zh-CN': zhCN, + }, + + themeConfig: { + socialLinks: [ + { icon: 'github', link: 'https://github.com/deepseek-harness/deepseek-harness' }, + ], + }, +}) diff --git a/website/.vitepress/config/zh-CN.ts b/website/.vitepress/config/zh-CN.ts new file mode 100644 index 0000000000..83767b6cbc --- /dev/null +++ b/website/.vitepress/config/zh-CN.ts @@ -0,0 +1,99 @@ +import type { DefaultTheme, LocaleSpecificConfig } from 'vitepress' + +const guideSidebar: DefaultTheme.SidebarItem[] = [ + { + text: '入门', + items: [ + { text: '介绍', link: '/zh-CN/guide/' }, + { text: '快速开始', link: '/zh-CN/guide/quickstart' }, + { text: '配置文件', link: '/zh-CN/guide/config' }, + ], + }, +] + +const developSidebar: DefaultTheme.SidebarItem[] = [ + { + text: '基础', + items: [ + { text: '第一个插件', link: '/zh-CN/develop/basic/' }, + { text: '开发一个 Tool', link: '/zh-CN/develop/basic/tool' }, + { text: '插件配置', link: '/zh-CN/develop/basic/config' }, + ], + }, + { + text: '框架能力', + items: [ + { text: '插件与生命周期', link: '/zh-CN/develop/framework/' }, + { text: '服务与依赖', link: '/zh-CN/develop/framework/service' }, + { text: '事件系统', link: '/zh-CN/develop/framework/events' }, + ], + }, + { + text: '实战', + items: [ + { text: '能力的三层拆分', link: '/zh-CN/develop/practice/' }, + { text: 'LLM 适配器', link: '/zh-CN/develop/practice/llm-adapter' }, + ], + }, +] + +const apiSidebar: DefaultTheme.SidebarItem[] = [ + { + text: '框架 API', + items: [ + { text: '总览', link: '/zh-CN/api/' }, + { text: 'Context', link: '/zh-CN/api/cordis/context' }, + { text: 'Events', link: '/zh-CN/api/cordis/events' }, + { text: 'Fiber', link: '/zh-CN/api/cordis/fiber' }, + { text: 'Registry', link: '/zh-CN/api/cordis/registry' }, + { text: 'Service', link: '/zh-CN/api/cordis/service' }, + ], + }, + { + text: 'Harness API', + items: [ + { text: 'Tools (dsh-tools)', link: '/zh-CN/api/harness/tools' }, + { text: 'LLM (dsh-llm)', link: '/zh-CN/api/harness/llm' }, + { text: 'Session (dsh-session)', link: '/zh-CN/api/harness/session' }, + { text: 'Agent (dsh-agent)', link: '/zh-CN/api/harness/agent' }, + { text: 'Bash (dsh-bash)', link: '/zh-CN/api/harness/bash' }, + { text: 'Filesystem (dsh-fs)', link: '/zh-CN/api/harness/fs' }, + { text: 'Subagent (dsh-subagent)', link: '/zh-CN/api/harness/subagent' }, + ], + }, +] + +const designSidebar: DefaultTheme.SidebarItem[] = [ + { + text: '系统设计', + items: [ + { text: '概述', link: '/zh-CN/design/' }, + { text: '可组合性与插件系统', link: '/zh-CN/design/composability' }, + { text: '作用与余作用', link: '/zh-CN/design/effects-coeffects' }, + { text: '可逆作用', link: '/zh-CN/design/revertible-effects' }, + { text: '响应式余作用', link: '/zh-CN/design/reactive-coeffects' }, + { text: '上下文模型', link: '/zh-CN/design/context-model' }, + ], + }, +] + +export const zhCN: LocaleSpecificConfig = { + label: '简体中文', + lang: 'zh-CN', + themeConfig: { + nav: [ + { text: '入门', link: '/zh-CN/guide/', activeMatch: '/zh-CN/guide/' }, + { text: '开发', link: '/zh-CN/develop/basic/', activeMatch: '/zh-CN/develop/' }, + { text: 'API', link: '/zh-CN/api/', activeMatch: '/zh-CN/api/' }, + { text: '设计', link: '/zh-CN/design/', activeMatch: '/zh-CN/design/' }, + ], + sidebar: { + '/zh-CN/guide/': guideSidebar, + '/zh-CN/develop/': developSidebar, + '/zh-CN/api/': apiSidebar, + '/zh-CN/design/': designSidebar, + }, + outline: { label: '本页目录' }, + docFooter: { prev: '上一篇', next: '下一篇' }, + }, +} diff --git a/website/package.json b/website/package.json new file mode 100644 index 0000000000..33c32fae4c --- /dev/null +++ b/website/package.json @@ -0,0 +1,15 @@ +{ + "name": "@deepseek-ai/website", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vitepress dev . --port 5173 --open", + "build": "vitepress build .", + "preview": "vitepress preview ." + }, + "devDependencies": { + "vitepress": "^1.6.3", + "vue": "^3.5.13" + } +} diff --git a/website/zh-CN/api/cordis/context.md b/website/zh-CN/api/cordis/context.md new file mode 100644 index 0000000000..a18f275dad --- /dev/null +++ b/website/zh-CN/api/cordis/context.md @@ -0,0 +1,85 @@ +# Context + +上下文对象是 Cordis 的核心。所有服务、方法、属性都通过 `ctx` 访问。 + +## 服务与混入 + +Context 基于组合式 API 设计,大部分属性和方法挂载在服务上。以下是核心 API: + +- [`ctx.on`](./events#ctx-on) — 注册事件监听器 +- [`ctx.emit`](./events#ctx-emit) — 触发事件 +- [`ctx.bail`](./events#ctx-bail) — 短路事件 +- [`ctx.serial`](./events#ctx-serial) — 顺序异步事件 +- [`ctx.waterfall`](./events#ctx-waterfall) — 管道事件 +- [`ctx.effect`](./fiber#fiber-effect) — 注册可逆效果 +- [`ctx.plugin`](./registry#ctx-plugin) — 加载子插件 +- [`ctx.inject`](./registry#ctx-inject) — 获取依赖的插件 +- [`ctx.get`](#ctx-get) — 获取服务 +- [`ctx.set`](#ctx-set) — 设置服务 +- [`ctx.provide`](#ctx-provide) — 声明服务 + +## 实例属性 + +### ctx.fiber + +- **类型:** [`Fiber`](./fiber) + +当前上下文的作用域对象。 + +## 实例方法 + +### ctx.extend(meta) + +- **meta:** `object` +- **返回值:** `Context` + +构造一个以当前上下文为原型的新上下文实例。 + +### ctx.intercept(name, config) + +- **name:** `string` 服务名称 +- **config:** `object` 配置拦截 +- **返回值:** `Context` + +为指定服务添加一层配置拦截,返回新的上下文实例。 + +### ctx.isolate(name, label?) + +- **name:** `string` 服务名称 +- **label:** `symbol` 隔离域符号(可选) +- **返回值:** `Context` + +创建一个针对指定服务的隔离域,返回新的上下文实例。隔离域中的同名服务互不影响。 + +### ctx.get(name) + +- **name:** `string` 服务名称 +- **返回值:** `Service | undefined` + +获取指定名称的服务实例。 + +### ctx.set(name, value) + +- **name:** `string` 服务名称 +- **value:** `any` 服务值 + +设置指定名称的服务。 + +### ctx.provide(name, value?, options?) + +- **name:** `string` 服务名称 +- **value:** `any` 初始值(可选) +- **options:** `object` +- **返回值:** `void` + +声明一个服务。声明后其他插件可以通过 `inject` 依赖它。 + +## 静态属性 + +### Context.events + +内置事件服务的 symbol key。 + +### Context.current + +当前活跃的 Context 实例(在异步链中通过 AsyncLocalStorage 追踪)。 diff --git a/website/zh-CN/api/cordis/events.md b/website/zh-CN/api/cordis/events.md new file mode 100644 index 0000000000..dbc03a87bc --- /dev/null +++ b/website/zh-CN/api/cordis/events.md @@ -0,0 +1,120 @@ +# Events + +`ctx.events` 是内置服务,提供事件系统相关的全部 API。 + +## 实例方法 + +### ctx.on(event, listener, options?) {#ctx-on} + +- **event:** `string` 事件名称 +- **listener:** `Function` 事件监听器 +- **options:** `object` + - **prepend:** `boolean` 是否注册为前置(默认 `false`) + - **global:** `boolean` 是否注册为全局(默认 `false`) +- **返回值:** `() => void` 取消注册函数 + +注册一个事件监听器。返回的函数可用于手动取消注册,但通常不需要——插件卸载时会自动清理。 + +```typescript +ctx.on('agent/turn-end', (data) => { + console.log('turn ended:', data) +}) +``` + +### ctx.emit(thisArg?, event, ...args) {#ctx-emit} + +- **thisArg:** `any` 监听器的 `this` 参数(可选) +- **event:** `string` 事件名称 +- **args:** `any[]` 事件参数 +- **返回值:** `void` + +同步触发所有匹配的监听器(并行,不等待异步完成)。 + +### ctx.parallel(thisArg?, event, ...args) + +- 签名同 `emit` +- **返回值:** `Promise` + +异步触发所有匹配的监听器(并行等待)。 + +### ctx.bail(thisArg?, event, ...args) {#ctx-bail} + +- **返回值:** `any` + +同步依次触发监听器。第一个返回非 `undefined`/`null`/`false` 值的监听器停止链并返回该值。 + +### ctx.serial(thisArg?, event, ...args) {#ctx-serial} + +- **返回值:** `Promise` + +异步依次触发监听器。语义同 `bail` 的异步版本。 + +### ctx.waterfall(thisArg?, event, ...args) {#ctx-waterfall} + +- **返回值:** `Promise` + +管道模式:每个监听器接收前一个的输出。监听器内部必须调用 `next()` 才会传递给下一个。 + +```typescript +// 注册 +ctx.on('llm/pre-request', async (messages, next) => { + messages.push(extraMsg) + return next(messages) // 必须调用 +}) + +// 触发 +const result = await ctx.waterfall('llm/pre-request', initialMessages) +``` + +::: warning +不调用 `next()` 即为否决 (veto)——管道终止。这是设计行为,用于拦截/网关。 +::: + +## Harness 内置事件 + +### agent/pre-step + +- **触发模式:** serial +- **参数:** `{ agentId, turnIndex }` + +Agent 执行一步之前触发。 + +### agent/post-step + +- **触发模式:** emit +- **参数:** `{ agentId, turnIndex, blocks }` + +Agent 执行一步之后触发。 + +### tool/call + +- **触发模式:** emit +- **参数:** `{ name, args, callId }` + +Tool 被模型调用时触发。 + +### tool/result + +- **触发模式:** emit +- **参数:** `{ name, result, callId }` + +Tool 返回结果时触发。 + +### session/event + +- **触发模式:** emit +- **参数:** `SessionEvent` + +会话事件被记录时触发。 + +### compact/start + +- **触发模式:** emit + +上下文压缩开始。 + +### compact/end + +- **触发模式:** emit + +上下文压缩结束。 diff --git a/website/zh-CN/api/cordis/fiber.md b/website/zh-CN/api/cordis/fiber.md new file mode 100644 index 0000000000..ffb8f23bb5 --- /dev/null +++ b/website/zh-CN/api/cordis/fiber.md @@ -0,0 +1,108 @@ +# Fiber + +Fiber(作用域)是插件实例的运行时容器,管理其生命周期和效果。 + +## 状态机 + +``` +PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED + ↘ FAILED +``` + +| 状态 | 数值 | 含义 | +|------|------|------| +| PENDING | 0 | 依赖未就绪,等待中 | +| LOADING | 1 | 正在执行 `apply` | +| ACTIVE | 2 | 运行中 | +| FAILED | 3 | `apply` 抛出异常 | +| UNLOADING | 4 | 正在撤销效果 | +| DISPOSED | 5 | 已完全卸载 | + +## 实例属性 + +### fiber.uid + +- **类型:** `number` + +Fiber 的唯一标识符。 + +### fiber.status + +- **类型:** `number` + +当前状态(见状态机)。 + +### fiber.config + +- **类型:** `object` + +传递给插件的配置对象。 + +### fiber.error + +- **类型:** `Error | undefined` + +如果状态是 FAILED,包含导致失败的异常。 + +## 实例方法 + +### fiber.effect(callback) {#fiber-effect} + +- **callback:** `() => (() => void) | void` +- **返回值:** `() => void` + +注册一个效果。`callback` 在 Fiber 激活时执行;如果返回函数,该函数在 Fiber dispose 时执行。 + +```typescript +ctx.effect(() => { + const timer = setInterval(tick, 1000) + return () => clearInterval(timer) +}) +``` + +等价地可以通过 `ctx.effect()` 调用(ctx 代理到当前 fiber)。 + +### fiber.dispose() + +- **返回值:** `Promise` + +手动 dispose 该 Fiber。按注册逆序撤销所有效果,递归 dispose 所有子 Fiber。 + +```typescript +const child = ctx.plugin(somePlugin) +// 之后: +await child.dispose() +``` + +### fiber.update(config) + +- **config:** `object` 新配置 +- **返回值:** `void` + +热更新配置。如果新旧配置不同,触发 dispose + 重新 apply。 + +### fiber.restart() + +- **返回值:** `void` + +强制重启:dispose 后重新加载。 + +### fiber.then(resolve, reject?) + +- **返回值:** `Promise` + +使 Fiber 可以被 `await`:等到状态进入 ACTIVE 或 FAILED。 + +```typescript +const fiber = ctx.plugin(myPlugin) +await fiber // 等待插件加载完成 +``` + +## 访问当前 Fiber + +```typescript +export function apply(ctx: Context) { + const fiber = ctx.fiber // 当前插件的 Fiber + console.log(fiber.status) // 1 (LOADING, 因为正在 apply 中) +} +``` diff --git a/website/zh-CN/api/cordis/registry.md b/website/zh-CN/api/cordis/registry.md new file mode 100644 index 0000000000..e0f66d8ed7 --- /dev/null +++ b/website/zh-CN/api/cordis/registry.md @@ -0,0 +1,87 @@ +# Registry + +插件注册表,管理插件的加载和依赖解析。 + +## 实例方法 + +### ctx.plugin(plugin, config?) {#ctx-plugin} + +- **plugin:** `Plugin` 插件(函数、对象或类) +- **config:** `object` 传递给插件的配置(可选) +- **返回值:** `Fiber` + +加载一个子插件,返回其 Fiber。子 Fiber 的生命周期绑定到父上下文。 + +```typescript +// 函数插件 +ctx.plugin(myPlugin, { key: 'value' }) + +// 类插件 +ctx.plugin(MyService) + +// 返回的 Fiber 可以 await 或 dispose +const fiber = ctx.plugin(myPlugin) +await fiber +``` + +### ctx.inject(names, callback) {#ctx-inject} + +- **names:** `string[]` 服务名列表 +- **callback:** `(ctx: Context) => void` +- **返回值:** `() => void` + +等待指定服务全部就绪后执行 callback。如果服务消失,callback 的效果会自动撤销;服务恢复后重新执行。 + +```typescript +ctx.inject(['tools', 'llm'], (ctx) => { + // tools 和 llm 都就绪了 + ctx.tools.register(/* ... */) +}) +``` + +这是 `export const inject = [...]` 声明的底层 API。大多数情况下直接使用声明式写法即可。 + +## 插件形态 + +`ctx.plugin()` 接受三种插件形态: + +### 函数插件 + +```typescript +function myPlugin(ctx: Context, config?: Config) { + // ... +} +myPlugin.name = 'my-plugin' +myPlugin.inject = ['tools'] +``` + +### 对象插件 + +```typescript +const myPlugin = { + name: 'my-plugin', + inject: ['tools'], + apply(ctx: Context, config?: Config) { + // ... + }, +} +``` + +### 类插件(Service) + +```typescript +class MyService extends Service { + static inject = ['tools'] + constructor(ctx: Context) { + super(ctx, 'myService') + } +} +``` + +## 插件元信息 + +| 属性 | 类型 | 说明 | +|------|------|------| +| `name` | `string` | 插件名称(日志用) | +| `inject` | `string[] \| { required?: string[], optional?: string[] }` | 依赖声明 | +| `Config` | `Schema \| object` | 配置 schema 或默认值 | diff --git a/website/zh-CN/api/cordis/service.md b/website/zh-CN/api/cordis/service.md new file mode 100644 index 0000000000..a57a00c461 --- /dev/null +++ b/website/zh-CN/api/cordis/service.md @@ -0,0 +1,97 @@ +# Service + +Service 基类,用于创建对外暴露能力的插件。 + +## 基本用法 + +```typescript +import { Service, type Context } from 'cordis' + +declare module 'cordis' { + interface Context { + myService: MyService + } +} + +export default class MyService extends Service { + constructor(ctx: Context) { + super(ctx, 'myService') + } + + // 公开方法 + doSomething() { + // ... + } +} +``` + +加载后,其他插件可通过 `ctx.myService` 访问。 + +## 构造函数 + +### new Service(ctx, name) + +- **ctx:** `Context` 上下文 +- **name:** `string` 服务名(注册到 `ctx[name]`) + +## 实例属性 + +### service.ctx + +- **类型:** `Context` + +该服务绑定的上下文。 + +### service\[Service.tracker\] + +- **类型:** `object` + +服务追踪信息(名称、绑定状态等)。 + +## 生命周期 + +Service 子类可以覆写以下方法: + +### start() + +服务激活时调用。在这里初始化资源。 + +### stop() + +服务停用时调用。在这里释放资源。 + +## 静态属性 + +### Service.inject + +- **类型:** `string[] | { required?: string[], optional?: string[] }` + +声明本服务依赖的其他服务。 + +## 与 inject 的关系 + +当一个 Service 被加载: +1. 框架为该服务名创建声明 (`ctx.provide`) +2. 实例赋值到 `ctx[name]` +3. 依赖该服务的所有 Fiber 从 PENDING 转为 LOADING + +当 Service 被卸载: +1. `ctx[name]` 被置为 `undefined` +2. 依赖它的 Fiber 被 dispose +3. 当新的 provider 出现时,dependant Fiber 重新加载 + +## 示例:Harness 中的 Service + +```typescript +// dsh-tools 的 ToolRegistry 就是一个 Service +export class ToolRegistry extends Service { + constructor(ctx: Context) { + super(ctx, 'tools') + } + + register(tool: ToolDefinition): () => void { + // ...注册逻辑 + return dispose + } +} +``` diff --git a/website/zh-CN/api/harness/agent.md b/website/zh-CN/api/harness/agent.md new file mode 100644 index 0000000000..bf46c7e4e1 --- /dev/null +++ b/website/zh-CN/api/harness/agent.md @@ -0,0 +1,85 @@ +# Agent (dsh-agent) + +Agent 实例管理和生命周期。 + +**包名:** `@deepseek-ai/dsh-agent` +**服务名:** `ctx.agents` + +## Agent Service + +### ctx.agents.create(options) + +- **options:** `AgentOptions` +- **返回值:** `Agent` + +创建一个新的 Agent 实例。 + +### ctx.agents.get(id) + +- **id:** `AgentId` +- **返回值:** `Agent | undefined` + +获取指定 ID 的 Agent 实例。 + +## AgentOptions + +```typescript +interface AgentOptions { + /** Agent ID(branded) */ + id?: AgentId + /** 使用的模型名 */ + model: string + /** 系统提示词(支持 {{model}} 变量) */ + persona?: string + /** 关联的 session */ + session?: Session +} +``` + +## Agent 实例 + +### agent.id + +- **类型:** `AgentId` + +Agent 的唯一标识符(branded string)。 + +### agent.model + +- **类型:** `string` + +Agent 使用的模型名。 + +### agent.step(input) + +- **input:** `ContentBlock[]` +- **返回值:** `Promise` + +执行一步:将输入发送给模型,获取响应,执行 tool calls。这是 agent-loop 内部使用的核心方法。 + +## Agent Loop + +Agent 的执行循环由 `dsh-agent-loop` 管理。它: + +1. 组装 system prompt + 历史消息 + 当前输入 +2. 调用 LLM(通过 `ctx.llm`) +3. 解析响应中的 tool calls +4. 执行 tools +5. 将 tool results 追加到 session +6. 如果 finish reason 是 `tool-calls`,回到步骤 2 + +### 扩展点 + +- `agent/pre-step` 事件 — 在每一步 LLM 调用前触发 +- `agent/post-step` 事件 — 在每一步完成后触发 +- `llm/pre-request` waterfall — 可修改发送给模型的消息 + +## AgentId + +Opaque branded string: + +```typescript +import { AgentId } from '@deepseek-ai/dsh-agent' + +const id = AgentId('main') +``` diff --git a/website/zh-CN/api/harness/bash.md b/website/zh-CN/api/harness/bash.md new file mode 100644 index 0000000000..8e8d8d3068 --- /dev/null +++ b/website/zh-CN/api/harness/bash.md @@ -0,0 +1,81 @@ +# Bash (dsh-bash) + +Bash 命令执行接口。 + +**接口包:** `@deepseek-ai/dsh-bash` +**实现:** `@deepseek-ai/dsh-bash-local` +**消费者:** `@deepseek-ai/dsh-tool-bash`(内置于 agent-core) + +## Bash Service + +### ctx.bash.execute(request) + +- **request:** `BashRequest` +- **返回值:** `Promise` + +执行一个 bash 命令。 + +## BashRequest + +```typescript +interface BashRequest { + /** 要执行的命令 */ + command: string + /** 工作目录 */ + workdir?: string + /** 超时时间 (ms) */ + timeoutMs?: number +} +``` + +## BashResult + +```typescript +interface BashResult { + /** 退出码 */ + exitCode: number + /** stdout 输出 */ + stdout: string + /** stderr 输出 */ + stderr: string + /** 是否超时 */ + timedOut: boolean +} +``` + +## 配置 (dsh-bash-local) + +```typescript +interface Config { + /** 命令超时时间,默认 120000 (2 分钟) */ + timeoutMs: number +} +``` + +在 `cordis.yml` 中: + +```yaml +- name: '@deepseek-ai/dsh-bash-local' + config: + timeoutMs: 60000 +``` + +## 模型可用的 Tools + +`dsh-tool-bash` 向模型暴露以下 tools(由 `agent-core` 捆绑): + +| Tool | 说明 | +|------|------| +| `bash` | 执行命令(同步,等待完成) | +| `bash_output` | 获取后台命令的输出 | +| `bash_kill` | 终止后台命令 | + +## 设计模式 + +Bash 是 Harness 的"能力三件套"典型案例: + +- `dsh-bash`(接口):定义 `ctx.bash` 和 `BashRequest`/`BashResult` 类型 +- `dsh-bash-local`(实现):通过 `child_process.spawn` 在本地执行 +- `dsh-tool-bash`(消费者):将能力包装为模型可调用的 tool + +换一个沙箱执行器只需替换 `dsh-bash-local`,接口和 tool 不变。 diff --git a/website/zh-CN/api/harness/fs.md b/website/zh-CN/api/harness/fs.md new file mode 100644 index 0000000000..4e336ff962 --- /dev/null +++ b/website/zh-CN/api/harness/fs.md @@ -0,0 +1,78 @@ +# Filesystem (dsh-fs) + +文件系统操作接口。 + +**接口包:** `@deepseek-ai/dsh-fs` +**实现:** `@deepseek-ai/dsh-fs-local` + `@deepseek-ai/dsh-fs-policy` +**消费者:** `@deepseek-ai/dsh-tool-fs` + +## FS Service + +### ctx.fs.read(path, options?) + +- **path:** `string` +- **options:** `{ offset?: number; limit?: number }` +- **返回值:** `Promise` + +读取文件内容。 + +### ctx.fs.write(path, content) + +- **path:** `string` +- **content:** `string` +- **返回值:** `Promise` + +写入文件(覆盖)。 + +### ctx.fs.edit(path, edits) + +- **path:** `string` +- **edits:** `Edit[]` +- **返回值:** `Promise` + +对文件执行精确的字符串替换编辑。 + +### ctx.fs.stat(path) + +- **path:** `string` +- **返回值:** `Promise` + +获取文件/目录信息。 + +## 配置 (dsh-fs-local) + +```typescript +interface Config { + /** 工作目录(相对路径的基准) */ + cwd: string +} +``` + +## 策略门 (dsh-fs-policy) + +`dsh-fs-policy` 是一个可选的中间层插件,实现 read-before-write/edit 策略——模型必须先读取文件才能写入或编辑。这防止模型盲目覆盖文件。 + +在 `cordis.yml` 中,它位于 `fs-local` 和 `tool-fs` 之间: + +```yaml +- name: '@deepseek-ai/dsh-fs-local' + config: + cwd: !!js process.cwd() +- name: '@deepseek-ai/dsh-fs-policy' +- name: '@deepseek-ai/dsh-tool-fs' +``` + +## 模型可用的 Tools + +| Tool | 说明 | +|------|------| +| `read` | 读取文件内容(支持 offset/limit) | +| `write` | 写入文件(需要先 read) | +| `edit` | 精确字符串替换(需要先 read) | + +## 三件套结构 + +- `dsh-fs`:接口定义 +- `dsh-fs-local`:本地文件系统实现 +- `dsh-fs-policy`:策略门(read-before-write 检查) +- `dsh-tool-fs`:模型 tool 层 diff --git a/website/zh-CN/api/harness/llm.md b/website/zh-CN/api/harness/llm.md new file mode 100644 index 0000000000..82a4d8e225 --- /dev/null +++ b/website/zh-CN/api/harness/llm.md @@ -0,0 +1,124 @@ +# LLM (dsh-llm) + +LLM 服务接口和适配器注册。 + +**包名:** `@deepseek-ai/dsh-llm` +**服务名:** `ctx.llm` + +## LLM Service + +### ctx.llm.registerAdapter(models, adapter) + +- **models:** `string[]` 该适配器支持的模型名列表 +- **adapter:** `LlmAdapter` 适配器实例 +- **返回值:** `() => void` disposer + +注册一个 LLM 适配器。当请求中指定的模型名在 `models` 列表中时,路由到该适配器。 + +```typescript +ctx.llm.registerAdapter(['deepseek-v4-flash', 'deepseek-v4-pro'], adapter) +``` + +## LlmAdapter + +适配器基类。子类必须实现 `stream()` 方法。 + +### stream(options) + +- **options:** `GenerateOptions` +- **返回值:** `AsyncIterable` + +将统一请求格式转换为具体 API 的流式调用。 + +## GenerateOptions + +```typescript +interface GenerateOptions { + model: string + messages: Message[] + tools?: ToolSpec[] + system?: string + maxTokens?: number + temperature?: number +} +``` + +| 字段 | 说明 | +|------|------| +| `model` | 请求的模型名 | +| `messages` | 对话历史 | +| `tools` | 当前可用的 tool 列表(JSON Schema 格式) | +| `system` | 系统提示词 | +| `maxTokens` | 最大输出 token | +| `temperature` | 采样温度 | + +## StreamChunk + +流式响应的增量 chunk 类型: + +```typescript +type StreamChunk = + | { type: 'block-start'; index: number; blockType: 'text' | 'tool-call' } + | { type: 'text-delta'; index: number; text: string } + | { type: 'tool-call-delta'; index: number; id: CallId; name: string; argumentsDelta: string } + | { type: 'block-end'; index: number; block: ContentBlock } + | { type: 'usage'; usage: TokenUsage } + | { type: 'finish'; reason: FinishReason } +``` + +### 协议规则 + +1. 每个内容块以 `block-start` 开始,以 `block-end` 结束 +2. `index` 从 0 递增 +3. `text-delta` 只在 `blockType: 'text'` 的块中 +4. `tool-call-delta` 只在 `blockType: 'tool-call'` 的块中 +5. `usage` 在 `finish` 之前 +6. `finish` 必须是最后一个 chunk + +## CallId + +Tool call 的 opaque branded ID: + +```typescript +import { CallId } from '@deepseek-ai/dsh-llm' + +const id = CallId('call-abc123') +``` + +## TokenUsage + +```typescript +interface TokenUsage { + inputTokens: number + outputTokens: number +} +``` + +## FinishReason + +```typescript +type FinishReason = + | { kind: 'stop' } + | { kind: 'tool-calls' } + | { kind: 'max-tokens' } +``` + +## Message + +对话消息类型: + +```typescript +interface Message { + role: 'user' | 'assistant' + content: ContentBlock[] +} +``` + +## ContentBlock + +```typescript +type ContentBlock = + | { type: 'text'; text: string } + | { type: 'tool-call'; id: CallId; name: string; arguments: string } + | { type: 'tool-result'; callId: CallId; content: ContentBlock[]; isError?: boolean } +``` diff --git a/website/zh-CN/api/harness/session.md b/website/zh-CN/api/harness/session.md new file mode 100644 index 0000000000..5ff5b0d97b --- /dev/null +++ b/website/zh-CN/api/harness/session.md @@ -0,0 +1,56 @@ +# Session (dsh-session) + +会话事件流管理。 + +**包名:** `@deepseek-ai/dsh-session` +**服务名:** `ctx.session` + +## 概述 + +Session 是 Agent 的对话状态容器。所有模型可见的内容都必须经过 session 事件流记录——这是"model-visible = logged"原则的实现。 + +## SessionSurface + +会话的外部接口,用于查询当前状态。 + +### surface.messages + +- **类型:** `Message[]` + +当前会话的完整消息列表(经过 compaction 处理后的视图)。 + +### surface.events + +- **类型:** `SessionEvent[]` + +原始事件流。 + +## SessionEvent + +会话中所有变更以事件形式记录: + +```typescript +type SessionEvent = + | { type: 'user/message'; content: ContentBlock[] } + | { type: 'assistant/message'; content: ContentBlock[] } + | { type: 'tool/call'; name: string; args: unknown; callId: CallId } + | { type: 'tool/result'; callId: CallId; content: ContentBlock[]; isError?: boolean } + | { type: 'compact/start'; range: [number, number] } + | { type: 'compact/end'; summary: string } + | { type: 'todo/write'; items: TodoItem[] } + // ... 更多事件类型 +``` + +## 设计原则 + +### Model-visible = Logged + +任何到达模型请求的内容都必须能从 session log 重建。如果你要引入新的模型可见输入,必须先定义对应的 session event。 + +### 事件是 append-only + +Session 事件流是只追加的。修改历史(如 compaction)通过新事件(compact/start + compact/end)表达,而不是修改旧事件。 + +### 持久化 + +Session 事件流可以通过 `dsh-session-persistence` 持久化到磁盘(JSONL 或 SQLite),实现跨进程恢复。 diff --git a/website/zh-CN/api/harness/subagent.md b/website/zh-CN/api/harness/subagent.md new file mode 100644 index 0000000000..97ad7b5c87 --- /dev/null +++ b/website/zh-CN/api/harness/subagent.md @@ -0,0 +1,85 @@ +# Subagent (dsh-subagent) + +子代理委派接口。 + +**接口包:** `@deepseek-ai/dsh-subagent` +**实现:** `@deepseek-ai/dsh-subagent-spawn` / `@deepseek-ai/dsh-subagent-fork` +**消费者:** `@deepseek-ai/dsh-tool-subagent` + +## Subagent Service + +### ctx.subagent.run(request) + +- **request:** `SubagentRequest` +- **返回值:** `Promise` + +委派一个任务给子代理执行。 + +## SubagentRequest + +```typescript +interface SubagentRequest { + /** 使用的 provider 名称 */ + provider: string + /** 委派给子代理的提示 */ + prompt: string + /** 子代理使用的模型(可选,默认继承父) */ + model?: string +} +``` + +## SubagentResult + +```typescript +interface SubagentResult { + /** 子代理的最终回复 */ + response: string +} +``` + +## Provider 模式 + +Subagent 支持多种"后端"(provider),通过配置选择: + +### spawn + +创建一个全新的子代理实例,没有父级的对话历史: + +```yaml +- name: '@deepseek-ai/dsh-subagent-spawn' + config: + providerName: spawn +``` + +### fork + +创建一个携带父级已完成 turn 前缀的子代理,子代理"知道"父级的对话上下文: + +```yaml +- name: '@deepseek-ai/dsh-subagent-fork' + config: + providerName: fork +``` + +## 模型可用的 Tools + +通过 `dsh-tool-subagent` 暴露。可以加载多次,每次绑定不同 provider: + +```yaml +# 暴露为 "subagent" tool,使用 spawn 后端 +- name: '@deepseek-ai/dsh-tool-subagent' + config: + provider: spawn + toolName: subagent + +# 暴露为 "subagent_fork" tool,使用 fork 后端 +- name: '@deepseek-ai/dsh-tool-subagent' + config: + provider: fork + toolName: subagent_fork +``` + +## 使用场景 + +- **spawn** — 独立子任务(如"搜索这个问题"),子代理不需要知道父级上下文 +- **fork** — 需要上下文的子任务(如"基于我们刚才讨论的,去实现这个"),子代理继承父级的对话前缀 diff --git a/website/zh-CN/api/harness/tools.md b/website/zh-CN/api/harness/tools.md new file mode 100644 index 0000000000..d2011ad85e --- /dev/null +++ b/website/zh-CN/api/harness/tools.md @@ -0,0 +1,122 @@ +# Tools (dsh-tools) + +Tool 注册表和 `defineTool` DSL。 + +**包名:** `@deepseek-ai/dsh-tools` +**服务名:** `ctx.tools` + +## ToolRegistry + +### ctx.tools.register(tool) + +- **tool:** `ToolDefinition` +- **返回值:** `() => void` disposer + +注册一个 tool。返回的 disposer 可手动撤销注册(通常不需要,插件卸载时自动撤销)。 + +## defineTool\(options) + +类型安全的 tool 定义辅助函数。 + +```typescript +import { defineTool } from '@deepseek-ai/dsh-tools' + +const tool = defineTool({ + name: 'read_file', + description: 'Read a file from disk.', + parameters: { + path: { type: 'string', required: true, description: 'Absolute file path' }, + offset: { type: 'number' }, + limit: { type: 'number', description: 'Max lines to read' }, + }, + async execute(args) { + // args: { path: string; offset?: number; limit?: number } + }, +}) +``` + +### DefineToolOptions\ + +| 字段 | 类型 | 说明 | +|------|------|------| +| `name` | `string` | Tool 名称(全局唯一) | +| `description` | `string` | 发送给模型的描述 | +| `parameters` | `SchemaSpec` | 参数 schema(见下文) | +| `execute` | `(args: InferArgs, exec: ToolExecution) => Promise` | 执行函数 | +| `presentCall?` | `(args: InferArgs) => ToolCallView \| undefined` | UI 展示(纯函数) | +| `presentResult?` | `(args: InferArgs, result: ToolResult) => ToolResultView \| undefined` | 结果 UI 展示(纯函数) | + +## SchemaSpec + +参数 schema DSL。每个属性是一个 `SchemaProp`: + +```typescript +interface SchemaProp { + type: 'string' | 'number' | 'boolean' | 'object' | 'array' + required?: true + description?: string + enum?: string[] + properties?: SchemaSpec // type: 'object' 时 + items?: SchemaProp // type: 'array' 时 +} +``` + +### 类型推导 (InferArgs) + +`InferArgs` 自动从 `SchemaSpec` 推导 TypeScript 类型: + +- `required: true` → 必填字段 +- 无 `required` → 可选字段(`?`) +- `type: 'object'` + `properties` → 递归推导嵌套对象 +- `type: 'array'` + `items` → 推导为数组 + +## ToolDefinition + +运行时 tool 定义(`defineTool` 的返回值): + +```typescript +interface ToolDefinition { + name: string + description: string + parameters: Record // JSON Schema + execute(args: unknown, exec: ToolExecution): Promise + presentCall?(args: unknown): ToolCallView | undefined + presentResult?(args: unknown, result: ToolResult): ToolResultView | undefined +} +``` + +## ToolExecuteReturn + +```typescript +type ToolExecuteReturn = + | ContentBlock[] // 仅内容 + | { content: ContentBlock[]; meta?: unknown } // 内容 + 元信息 +``` + +## ToolArgsError + +当模型生成的参数不匹配 schema 时抛出: + +```typescript +class ToolArgsError extends HarnessError { + code: 'INVALID_ARGS' + violations: string[] +} +``` + +框架自动捕获并转换为 `isError` 结果返回给模型。 + +## validateArgs(spec, args) + +- **spec:** `SchemaSpec` +- **args:** `unknown` +- **返回值:** `string[]` 违规信息列表(空 = 合法) + +手动校验参数。`defineTool` 内部使用,通常不需要直接调用。 + +## schemaSpecToJsonSchema(spec) + +- **spec:** `SchemaSpec` +- **返回值:** `JsonSchemaObject` + +将 SchemaSpec 转换为标准 JSON Schema。用于发送给模型的 wire format。 diff --git a/website/zh-CN/api/index.md b/website/zh-CN/api/index.md new file mode 100644 index 0000000000..371cd1e622 --- /dev/null +++ b/website/zh-CN/api/index.md @@ -0,0 +1,25 @@ +# API 参考 + +本节提供 DeepSeek Harness 的完整 API 参考文档,分为两部分: + +## 框架 API + +Cordis 微内核提供的基础能力,所有插件开发都建立在这些 API 之上: + +- [Context](./cordis/context) — 上下文对象,所有服务和方法的入口 +- [Events](./cordis/events) — 事件系统 API(emit / on / bail / serial / waterfall) +- [Fiber](./cordis/fiber) — 作用域生命周期(状态机、effect、dispose) +- [Registry](./cordis/registry) — 插件注册(plugin / inject) +- [Service](./cordis/service) — 服务基类 + +## Harness API + +DeepSeek Harness SDK 提供的扩展 API,用于构建 Agent 能力: + +- [Tools (dsh-tools)](./harness/tools) — Tool 注册、defineTool DSL、Schema 类型系统 +- [LLM (dsh-llm)](./harness/llm) — LLM 服务、适配器注册、StreamChunk 协议 +- [Session (dsh-session)](./harness/session) — 会话事件流、消息类型 +- [Agent (dsh-agent)](./harness/agent) — Agent 实例管理、生命周期 +- [Bash (dsh-bash)](./harness/bash) — Bash 执行接口 +- [Filesystem (dsh-fs)](./harness/fs) — 文件系统接口 +- [Subagent (dsh-subagent)](./harness/subagent) — 子代理委派接口 diff --git a/website/zh-CN/design/composability.md b/website/zh-CN/design/composability.md new file mode 100644 index 0000000000..8370d8e136 --- /dev/null +++ b/website/zh-CN/design/composability.md @@ -0,0 +1,72 @@ +# 可组合性与插件系统 + +## 组合 + +编程的本质就是组合。将小的构建块拼装为更大的系统,再将大系统作为块继续拼装——这是从函数到模块到微服务一脉相承的思想。 + +组合可以分为两种: + +- **静态组合**:编译期确定的组合,例如函数调用、模块导入。 +- **动态组合**:运行时确定的组合,例如热更新、插件加载/卸载。 + +静态组合是逻辑的组合;动态组合为可组合性引入了时间和空间两个新维度。 + +## 三种可组合性 + +| 维度 | 定义 | 对应问题 | +|------|------|----------| +| **逻辑可组合性** (Logical) | 功能能否被任意拆分和组装 | 接口设计是否正交 | +| **时间可组合性** (Temporal) | 能否灵活、安全地控制组合的运行时序 | 能否热加载/卸载而不泄漏 | +| **空间可组合性** (Spatial) | 能否灵活、安全地管理组合的依赖关系 | 依赖缺失时行为是否确定 | + +一门编程语言或应用框架越多地使用组合范式,就称它的可组合性越好。 + +## 传统插件系统的问题 + +插件系统是动态组合的典型形式。浏览器扩展、IDE 插件、操作系统驱动,都是其实例。然而大多数插件系统并不可靠。 + +### 不可逆的插件化 + +以 VSCode 为例: + +- 卸载或更新插件时需要重启整个系统。 +- 无法在运行时追踪和回收副作用,导致内存泄漏和非预期的资源占用。 +- 即便提供了 `deactivate` 钩子,也无法强制开发者正确实现清理逻辑。 + +**根本原因**:未做到时间可组合——系统不知道某个插件产生了哪些副作用、占用了哪些资源。 + +### 不完全的插件化 + +- 无法表达插件间的依赖关系,扩展能力受限。 +- 只有外围功能被下放给插件,核心功能依然通过修改主体代码来实现。 + +**根本原因**:未做到空间可组合——系统缺乏对依赖关系的建模和管理。 + +## Cordis 的解法 + +Cordis 同时解决了上述两个问题: + +1. **可逆作用** (Revertible Effects) 实现时间可组合性——所有注册自动追踪、自动回收。 +2. **响应式余作用** (Reactive Coeffects) 实现空间可组合性——依赖声明驱动加载顺序。 + +两者通过**上下文模型** (Context Model) 统一为单一的编程范式:开发者只需通过 `ctx` 调用框架 API,可逆性和依赖管理由框架保证。 + +## 在 Harness 中的体现 + +DeepSeek Harness 将 Cordis 的可组合性应用到 Agent 开发领域: + +```typescript +// 一个 Harness 插件天然是可逆的 +export const inject = ['tools', 'llm'] // 空间可组合:声明依赖 + +export function apply(ctx: Context) { + // 时间可组合:注册会被自动追踪和回收 + ctx.tools.register(defineTool('my-tool', { + description: '...', + parameters: { /* ... */ }, + async execute(args) { /* ... */ }, + })) +} +``` + +插件卸载时,tool 自动注销、事件监听自动移除——无需手动清理。依赖的服务(如 `llm`)消失时,插件自动挂起;恢复时自动重新加载。 diff --git a/website/zh-CN/design/context-model.md b/website/zh-CN/design/context-model.md new file mode 100644 index 0000000000..cc25df88e5 --- /dev/null +++ b/website/zh-CN/design/context-model.md @@ -0,0 +1,129 @@ +# 上下文模型 + +上下文 (Context) 是 Cordis 将作用与余作用统一的运行时模型。它提供了一种编程范式,允许开发者无心智负担地编写时间、空间可组合的程序。 + +## 作用上下文 (Effect Context) + +当副作用被记录到全局环境时,$\mathcal{C}\times\left(\mathcal{C}\to\mathcal{C}\right)$ 也就变成了一个更大的 $\mathcal{C}$。 + +递归地定义: + +$$ +\begin{matrix} +\mathcal{C}_1=\mathcal{C}_0\times\left(\mathcal{C}_0\to\mathcal{C}_0\right)\\ +\mathcal{C}_2=\mathcal{C}_1\times\left(\mathcal{C}_1\to\mathcal{C}_1\right)\\ +\cdots\\ +\mathcal{C}_{n+1}=\mathcal{C}_n\times\left(\mathcal{C}_n\to\mathcal{C}_n\right)\\ +\end{matrix} +$$ + +每一层 $\mathcal{C}$ 包含上一层的状态,同时记录了上一层的副作用。 + +利用递归类型得到真正的作用上下文: + +$$ +\mathcal{C}=\mathcal{C}\times\left(\mathcal{C}\to\mathcal{C}\right) +$$ + +这就是 Cordis Context 的理论根基:**上下文既是状态容器,又是副作用追踪器。** + +## 上下文的派生 + +当一个插件被加载时,从当前上下文派生出新的上下文实例: + +``` +Root Context +├── Plugin A Context ← 管理 A 的副作用 +│ └── Sub-plugin Context +└── Plugin B Context ← 管理 B 的副作用 +``` + +- 子级上下文管理插件内部的全部副作用 +- 插件整体作为一个副作用被父级上下文收集 +- 父级 dispose 时,子级先被 dispose(保证依赖逆序) + +## 余作用上下文 (Coeffect Context) + +余作用由作用产生: + +- **提供服务**本身是一种作用——它占用了服务命名空间资源 +- 因此服务的提供被记录在作用上下文中 +- 上下文将作用与余作用关联起来,提供了统一的时间、空间可组合性 + +```typescript +// 提供服务 = 一个 effect(占用 ctx.llm 这个 "资源") +class LlmService extends Service { + // 当此插件卸载时,ctx.llm 被回收(effect 的逆操作) + // 所有依赖 llm 的插件因 coeffect 不满足而挂起 +} +``` + +## 基于上下文的开发范式 + +上下文模型提供了两个关键优势: + +### 无感性 (Transparent) + +框架将领域中的所有方法都封装为 effect 版本。开发者只需调用 `ctx` 上的方法,就能自动获得时间/空间可组合性: + +```typescript +export function apply(ctx: Context) { + // 以下每一行都是 effect——卸载时自动逆序回收 + ctx.on('agent/step-result', validateResult) + ctx.tools.register(myTool) + ctx.llm.registerAdapter(['my-model'], adapter) + + // 开发者无需知道"可逆作用"的存在 + // 只需通过 ctx 调用,框架保证一切安全 +} +``` + +### 渐进性 (Incremental) + +可以逐步将现有框架中的 API 替换为可组合版本,无需一次性重写: + +```typescript +// 第一步:用 ctx.effect 包装遗留 API +ctx.effect(() => { + const legacy = legacySystem.register(handler) + return () => legacySystem.unregister(legacy) +}) + +// 第二步:在未来将遗留 API 原生改造为 effect +// 两种方式可以并存 +``` + +## 在 Harness 中的完整图景 + +DeepSeek Harness 的运行时是一个 Context 树: + +``` +Root Context (Cordis 应用) +├── dsh-session (提供 ctx.sessions) +├── dsh-tools (提供 ctx.tools) +├── dsh-llm (提供 ctx.llm) +│ └── deepseek-adapter (注册模型适配器) +├── dsh-agent-loop (提供 ctx.agentLoop) +├── dsh-bash (提供 ctx.bash) +│ └── bash-local (本地执行器实现) +├── dsh-fs (提供 ctx.fs) +│ └── fs-local (本地 FS 实现) +├── dsh-system-prompt (提供 ctx.systemPrompt) +└── Agent Context (由 agents.create() 派生) + ├── Agent 自己注册的 tools + ├── Agent 的 session + └── Subagent Context (进一步派生) +``` + +每个节点都是一个 Context 实例。插件加载/卸载、服务出现/消失、Agent 创建/销毁——这一切都在 Context 树上以统一的语义发生。 + +## 总结 + +| 概念 | 解决的问题 | Cordis 机制 | +|------|-----------|-------------| +| 作用上下文 | 副作用追踪与回收 | `ctx.effect()` / `fiber.dispose()` | +| 上下文派生 | 副作用的层级隔离 | `ctx.plugin()` 创建子 Context | +| 余作用上下文 | 依赖的动态管理 | `inject` 声明 + 服务生命周期 | +| 统一范式 | 开发者无需关心底层机制 | 只需通过 `ctx` 调用 API | + +这就是为什么 Harness 能在保持「一切皆插件」的同时,不给插件开发者增加心智负担——**上下文模型把复杂性封装在了框架内部**。 diff --git a/website/zh-CN/design/effects-coeffects.md b/website/zh-CN/design/effects-coeffects.md new file mode 100644 index 0000000000..01c181315f --- /dev/null +++ b/website/zh-CN/design/effects-coeffects.md @@ -0,0 +1,69 @@ +# 作用与余作用 + +## 作用 (Effects) + +Effects 是程序中对系统状态或外部环境产生影响的操作:I/O、状态修改、资源占用等。 + +学术界对作用有两种主要建模方式: + +### 单子作用 (Monadic Effects) + +- 通过单子 (monad) 将副作用封装为类型安全的计算链。 +- 提供 `return`(纯值注入)和 `bind`(链式组合)两个基本操作。 +- 以纯函数式的方式处理带有副作用的计算。(Moggi 1991, Wadler 1992) +- 代表语言:Haskell (IO Monad)、Rust (Result/Option) + +### 代数作用 (Algebraic Effects) + +- 允许在函数中"抛出"一个 effect,在调用栈的更高层次"捕获"并处理。 +- 类似异常处理,但更通用——处理后可以恢复执行。 +- 代表语言:Koka、Eff、OCaml 5+ (Kiselyov 2018, Kawahara 2020) + +## 余作用 (Coeffects) + +Coeffects 是程序执行时依赖的上下文信息:环境变量、系统资源、外部服务等。 + +- Coeffects 是 effects 的对偶 (dual) 概念,通常通过余单子 (comonad) 建模。(Petricek 2013, 2014; Brünnler 2014) +- 更前沿的理论将带有资源的上下文建模为 **graded algebra**(有序半环加最大元): + - 加法 = 并行组合;0 元 = 无资源 + - 乘法 = 串行组合;1 元 = 单位资源 + - 序 = 资源约束;最大元 = 无限资源 + - (Breuvart 2015, Gaboardi 2016, Dal Lago 2022) + +## 现有理论的不足 + +这些理论主要面向**静态分析**和**短时程序**: + +1. **缺乏运行时追踪**:类型系统能标记副作用的存在,但无法在运行时追踪和回收。对长时运行程序(服务端、Agent),这意味着资源泄漏不可避免。 + +2. **缺乏动态性**:面向编译期分析,无法处理运行时的加载/卸载需求。 + +3. **崩溃而非降级**:类型不满足时直接拒绝编译或运行时崩溃,而长时运行程序更希望安全降级——挂起不满足依赖的部分,而非停止整个系统。 + +## Cordis 的突破 + +Cordis 选择了不同的路径——在运行时层面解决可组合性问题: + +| 现有理论 | Cordis 方案 | +|----------|-------------| +| 类型标记副作用 | 运行时追踪并自动回收副作用 | +| 编译期拒绝 | 运行时挂起/恢复 | +| 面向短时程序 | 面向长时运行程序设计 | + +这由两个互补机制实现: + +- **[可逆作用](./revertible-effects)** — 将副作用形式化为可逆的群操作 +- **[响应式余作用](./reactive-coeffects)** — 将依赖建模为具有生命周期的服务 + +## 在 Agent 开发中的意义 + +对 DeepSeek Harness 而言,作用/余作用模型直接支撑了以下能力: + +| 作用 (Effect) | 余作用 (Coeffect) | +|---------------|-------------------| +| 注册一个 tool | 依赖 tool registry 服务 | +| 注册一个 LLM adapter | 依赖 LLM 服务接口 | +| 监听 session 事件 | 依赖 session 服务存在 | +| 启动子进程 | 依赖 bash executor 实现 | + +每一个 effect 都可逆(tool 可注销、adapter 可移除);每一个 coeffect 都有生命周期(服务消失则依赖者挂起)。这就是 Agent 能被安全热替换的根本原因。 diff --git a/website/zh-CN/design/index.md b/website/zh-CN/design/index.md new file mode 100644 index 0000000000..de6ebcf7aa --- /dev/null +++ b/website/zh-CN/design/index.md @@ -0,0 +1,39 @@ +# 系统设计 + +DeepSeek Harness 建立在 Cordis 微内核之上,采用「一切皆插件」的架构。本节阐述这套设计背后的理论基础和设计哲学。 + +## 核心思想 + +Harness 追求三种可组合性的统一: + +| 维度 | 含义 | Cordis 对应机制 | +|------|------|----------------| +| 逻辑可组合性 | 功能能否自由拆分和拼装 | 插件系统、事件系统 | +| 时间可组合性 | 运行时能否安全地加载/卸载功能 | 可逆作用、自动清理 | +| 空间可组合性 | 依赖关系能否被安全地声明和管理 | 服务生命周期、依赖注入 | + +这三种可组合性在上下文模型中统一为单一的编程范式。 + +## 目录 + +- [可组合性与插件系统](./composability) — 组合的本质,以及传统插件系统为什么不可靠 +- [作用与余作用](./effects-coeffects) — Cordis 效果系统的理论模型 +- [可逆作用](./revertible-effects) — 时间可组合性的形式化定义与证明 +- [响应式余作用](./reactive-coeffects) — 空间可组合性的服务语义 +- [上下文模型](./context-model) — Context 如何将作用与余作用统一 + +## 设计如何映射到 Harness + +| 理论概念 | Harness 中的体现 | +|----------|-----------------| +| 可逆作用 | `ctx.tools.register()` 返回 disposer;插件卸载时工具自动注销 | +| 响应式余作用 | `inject: ['llm']` 声明依赖;LLM 适配器不可用时插件自动挂起 | +| 上下文派生 | 子 Agent 拥有独立 Context,继承父级服务但有独立生命周期 | +| Waterfall 事件 | `agent/request` 链式拦截,任一监听器可决定最终请求参数 | +| Capability seam | bash/fs/web 三层拆分:接口 → 实现 → 模型工具 | + +## 进一步阅读 + +- [插件与生命周期](/zh-CN/develop/framework/) — 实践中的 Fiber 状态机 +- [服务与依赖](/zh-CN/develop/framework/service) — 服务声明与注入 +- [能力的三层拆分](/zh-CN/develop/practice/) — Capability seam 模式 diff --git a/website/zh-CN/design/reactive-coeffects.md b/website/zh-CN/design/reactive-coeffects.md new file mode 100644 index 0000000000..45345f934a --- /dev/null +++ b/website/zh-CN/design/reactive-coeffects.md @@ -0,0 +1,90 @@ +# 响应式余作用 + +响应式余作用 (Reactive Coeffects) 是 Cordis 实现**空间可组合性**的核心机制。 + +- 将代码中的资源依赖抽象为服务 (service) 的概念 +- 通过运行时生命周期语义,实现自动、安全、高效的资源管理 + +## 依赖的本质是生命周期 + +传统的依赖注入(如 Angular DI、Spring IoC)解决的是"怎么拿到依赖"的问题,但忽略了一个关键问题:**依赖是有生命周期的**。 + +一个数据库连接池可能重启,一个 API 服务可能下线,一个 LLM adapter 可能被热替换。当依赖消失时,依赖者应当如何表现? + +- 崩溃?——对长时运行程序不可接受。 +- 继续运行?——可能产生不一致状态。 +- **自动挂起,等待恢复?**——Cordis 的选择。 + +## 服务与生命周期 + +Cordis 将程序中的资源依赖抽象为**服务** (service): + +- 任何插件都可以声明自己依赖的服务列表 +- 服务存在明确的生命周期(提供、撤销) +- 运行时对依赖不满足的插件**等待**,而非拒绝 +- 服务生命周期结束前,依赖该服务的插件**先一步被回收** + +```typescript +// LLM 适配器插件:提供 llm 服务 +export class LlmService extends Service { + static inject = ['http'] // 自身依赖 http + // 当 http 不可用时,LlmService 自动挂起 + // 挂起导致 ctx.llm 不可用 + // 所有 inject: ['llm'] 的插件级联挂起 +} +``` + +## 与现有理论的对比 + +### 与 Comonad 余作用比较 + +基于 Comonad 的余作用(Petricek 2013)将上下文建模为静态结构,侧重于编译期分析。Cordis 的响应式余作用额外引入了**时序语义**: + +- 服务可在运行时出现/消失 +- 依赖关系随之动态建立/解除 +- 效果的生命周期由依赖关系决定 + +### 与 Grade Algebra 余作用比较 + +基于 Grade Algebra 的余作用(Gaboardi 2016)用有序半环描述资源的组合规则。Cordis 的服务依赖可以建模为**交换半群**: + +- 服务名构成依赖集合 +- 集合并(∪)对应并行依赖 +- 交换律:依赖 A + B ≡ 依赖 B + A(声明顺序无关) +- 结合律:依赖分组方式不影响语义 + +但 Cordis 还增加了代数不具备的运行时行为:当集合中的某个服务不可用时,整个依赖集不满足,触发挂起。 + +## 在 Cordis 中的实现 + +```typescript +// 声明依赖 +export const inject = ['tools', 'llm'] + +export function apply(ctx: Context) { + // 到这里时,ctx.tools 和 ctx.llm 一定可用 + // 如果任一服务消失,此插件自动卸载 + // 服务恢复后,自动重新执行 apply +} +``` + +服务生命周期变化时的行为: + +``` +llm service 可用 → 依赖 llm 的插件 PENDING → ACTIVE +llm service 消失 → 依赖 llm 的插件 ACTIVE → DISPOSED +llm service 恢复 → 依赖 llm 的插件重新 PENDING → ACTIVE +``` + +## 为什么 Agent 需要响应式余作用 + +在 Harness 场景下,响应式余作用直接支撑: + +| 场景 | 行为 | +|------|------| +| LLM adapter 热替换 | 依赖 `llm` 的插件自动挂起/恢复,中间不丢状态 | +| 按需加载 bash 执行器 | bash tool 只在 `bash` 服务就绪后注册 | +| 子 Agent 独立服务空间 | 通过 `ctx.isolate()` 隔离服务实例,互不干扰 | +| 可选能力降级 | `inject: { web: { required: false } }` 允许 web 不可用时继续运行 | + +这意味着 Harness 插件开发者无需编写防御性的 "if service exists" 检查——框架保证:当你的 `apply` 被调用时,声明的依赖一定已就绪。 diff --git a/website/zh-CN/design/revertible-effects.md b/website/zh-CN/design/revertible-effects.md new file mode 100644 index 0000000000..5133400e75 --- /dev/null +++ b/website/zh-CN/design/revertible-effects.md @@ -0,0 +1,128 @@ +# 可逆作用 + +可逆作用 (Revertible Effects) 是 Cordis 实现**时间可组合性**的核心机制。 + +- 在单子作用的基础上增加可逆性约束 +- 提供面向长时运行程序的作用系统 +- 确保程序可以在插件粒度上回到任意状态 + +## 副作用的封装 + +现实中的程序需要与各种副作用打交道。假设一个不纯函数: + +$$ +f_\text{impure}: \text{X}\to\text{Y} +$$ + +我们将所有可能的副作用用类型 $\mathcal{C}$ 封装,函数变为: + +$$ +f: \mathcal{C}\times\text{X}\to\mathcal{C}\times\text{Y} +$$ + +对于长时运行程序,忽略函数本身的入参和出参,$f$ 属于函数空间 $\mathfrak{F}=\mathcal{C}\to\mathcal{C}$。 + +## 从幺半群到群 + +任何函数 $f: \mathcal{C}\to\mathcal{C}$ 都是状态空间到自身的变换。在组合 $\circ$ 下构成**幺半群**: + +1. 封闭性:$f\circ g$ 也是 $\mathcal{C}\to\mathcal{C}$ +2. 结合律:$(f\circ g)\circ h=f\circ (g\circ h)$ +3. 单位元:$\text{id}$,使得 $f\circ\text{id}=\text{id}\circ f=f$ + +如果额外要求每个 $f$ 存在逆元 $f^{-1}$(即副作用可回收),$\mathfrak{F}$ 升级为**群**。 + +## 副作用都可逆吗? + +观察计算机中的副作用模式: + +| 操作 | 占用资源 | 逆操作 | +|------|----------|--------| +| 打开文件 | 文件描述符 | 关闭文件 | +| 创建子进程 | 进程号 | 杀死进程 | +| 监听端口 | 端口 | 取消监听 | +| 添加回调函数 | 事件槽位 | 删除回调 | +| 分配内存 | 内存区块 | 回收内存 | + +**副作用就是对资源的占用。** 计算机的资源天然设计为可重复使用,因此这些副作用一定是可逆的。 + +## 追踪和回收副作用 + +Cordis 通过 $\text{effect}$ 和 $\text{restore}$ 函子追踪和回收逆函数。 + +### effect 函子 + +$$ +\begin{array}{} +\text{effect}&:& +\left(\mathcal{C}\to\mathcal{C}\right)&\to& +\mathcal{C}\times\left(\mathcal{C}\to\mathcal{C}\right)&\to& +\mathcal{C}\times\left(\mathcal{C}\to\mathcal{C}\right)\\ +\text{effect}&=&f&\mapsto&\left(c, h\right)&\mapsto&\left(f(c), h\circ f^{-1}\right) +\end{array} +$$ + +直觉:执行 $f$ 产生的副作用记入状态 $c$,同时将逆操作 $f^{-1}$ 追加到回收链 $h$ 中。 + +### 同态性证明 + +$\text{effect}$ 是从 $\mathcal{C}\to\mathcal{C}$ 到 $\mathcal{C}\times(\mathcal{C}\to\mathcal{C})\to\mathcal{C}\times(\mathcal{C}\to\mathcal{C})$ 的同态: + +$$ +\begin{aligned} +\text{effect}\ (f\circ g) \left(c, h\right) +&=\left((f\circ g)(c), h\circ (f\circ g)^{-1}\right)\\ +&=\left(f(g(c)), h\circ g^{-1}\circ f^{-1}\right)\\ +&=\left(\text{effect}\ f\right)\left(g(c), h\circ g^{-1}\right)\\ +&=\left(\text{effect}\ f\right)\circ\left(\text{effect}\ g\right) \left(c, h\right) +\end{aligned} +$$ + +这意味着:组合两个操作后再追踪 = 分别追踪后再组合。副作用追踪与执行顺序无关。 + +### restore 函子 + +$$ +\begin{array}{} +\text{restore}&:& +\mathcal{C}\times\left(\mathcal{C}\to\mathcal{C}\right)&\to& +\mathcal{C}\times\left(\mathcal{C}\to\mathcal{C}\right)\\ +\text{restore}&=&\left(c, h\right)&\mapsto&\left(h(c),\text{id}\right) +\end{array} +$$ + +直觉:将回收链 $h$ 应用到当前状态,一次性回收所有已追踪的副作用。 + +## 在 Cordis 中的实现 + +理论映射到 API: + +| 数学概念 | Cordis API | 说明 | +|----------|-----------|------| +| $\text{effect}(f)$ | `ctx.effect(() => { ...; return dispose })` | 注册副作用并返回清理函数 | +| $\text{restore}$ | `fiber.dispose()` | 执行 Fiber 的整个回收链 | +| $f^{-1}$ | dispose 返回值 / cleanup 函数 | 逆操作 | + +```typescript +export function apply(ctx: Context) { + // effect: 创建资源,返回其逆操作 + ctx.effect(() => { + const server = startServer(8080) // f: 占用端口 + return () => server.close() // f⁻¹: 释放端口 + }) + + // 框架 API 内部已封装 effect + ctx.on('event', handler) // 内部: effect(addListener, removeListener) + ctx.tools.register(myTool) // 内部: effect(addTool, removeTool) +} +// 当此插件被卸载时,restore 自动按逆序执行所有 f⁻¹ +``` + +## 为什么 Agent 需要可逆作用 + +在 Harness 场景下,可逆作用直接支撑: + +- **热替换 LLM 适配器**:卸载旧适配器(回收注册)、加载新适配器,无需重启 +- **动态 tool 管理**:根据对话上下文动态添加/移除 tool,不泄漏 +- **子 Agent 生命周期**:子 Agent 完成后,其注册的所有临时 tool 和监听器自动清理 +- **优雅关闭**:进程退出时所有插件按依赖逆序 dispose,确保资源完全释放 diff --git a/website/zh-CN/develop/basic/config.md b/website/zh-CN/develop/basic/config.md new file mode 100644 index 0000000000..49bcc4ca77 --- /dev/null +++ b/website/zh-CN/develop/basic/config.md @@ -0,0 +1,108 @@ +# 插件配置 + +让你的插件接受用户在 `cordis.yml` 中传入的配置。 + +## 定义 Config 类型 + +在插件中导出一个 `Config` 类型和可选的默认值: + +```typescript +import type { Context } from 'cordis' + +export const name = 'my-plugin' + +export interface Config { + greeting: string + maxRetries: number + verbose?: boolean +} + +export const Config = { + greeting: 'Hello', + maxRetries: 3, + verbose: false, +} + +export function apply(ctx: Context, config: Config) { + console.log(config.greeting) // 用户配置或默认值 +} +``` + +用户在 `cordis.yml` 中这样使用: + +```yaml +- name: './src/my-plugin.ts' + config: + greeting: 'Hi there' + maxRetries: 5 +``` + +未提供的字段使用导出的 `Config` 对象中的默认值。 + +## Schema 校验 + +对于需要严格校验的场景,使用 Schemastery 定义 schema: + +```typescript +import type { Context } from 'cordis' +import Schema from 'schemastery' + +export const name = 'validated-plugin' + +export interface Config { + apiKey: string + timeout: number + mode: 'fast' | 'accurate' +} + +export const Config = Schema.object({ + apiKey: Schema.string().required(), + timeout: Schema.number().default(30000), + mode: Schema.union(['fast', 'accurate']).default('fast'), +}) + +export function apply(ctx: Context, config: Config) { + // config 已经过校验,类型安全 +} +``` + +Schema 在插件加载时执行校验。如果配置不合法,插件会加载失败并给出明确错误信息。 + +## 设计原则 + +### 无硬编码可调参数 + +Harness 的约定:**任何两个部署可能想要不同值的东西,都应该是配置字段**。 + +```typescript +// 错误 — 硬编码超时时间 +const TIMEOUT = 30000 + +// 正确 — 可配置 +export interface Config { + timeoutMs: number // 默认 30000 +} +``` + +检验标准:能否在 `cordis.yml` 中改变这个值,而不需要修改代码? + +### 配置错误要响亮 + +如果配置引用了不存在的东西(比如一个不存在的模型名),应该尽早报错,而不是静默跳过: + +```typescript +export function apply(ctx: Context, config: Config) { + if (!ctx.llm.hasAdapter(config.model)) { + throw new Error(`Model "${config.model}" is not registered by any LLM adapter`) + } +} +``` + +## 配合 HMR + +配置变更会触发插件热替换:修改 `cordis.yml` 中某个插件的 `config`,框架会卸载旧实例、加载新实例。由于注册都是效果(自动清理),这个过程是安全的。 + +## 下一步 + +- [插件与生命周期](../framework/) — 深入了解插件的完整生命周期 +- [服务与依赖](../framework/service) — 让你的插件对外提供服务 diff --git a/website/zh-CN/develop/basic/index.md b/website/zh-CN/develop/basic/index.md new file mode 100644 index 0000000000..71d6962edd --- /dev/null +++ b/website/zh-CN/develop/basic/index.md @@ -0,0 +1,148 @@ +# 第一个插件 + +本文带你编写一个最小的 Harness 插件并加载到 Agent 中。 + +## 插件是什么 + +在 Harness 中,插件是一个导出 `apply` 函数的 TypeScript 模块。框架在加载时调用 `apply`,传入一个 `ctx`(上下文对象),你通过 `ctx` 注册能力: + +```typescript +import type { Context } from 'cordis' + +export const name = 'my-plugin' + +export function apply(ctx: Context) { + // 在这里注册能力 +} +``` + +就这么简单。 + +## 创建插件文件 + +在你的项目目录下创建 `src/my-plugin.ts`: + +```typescript +import type { Context } from 'cordis' + +export const name = 'hello-plugin' + +export function apply(ctx: Context) { + // 监听 agent-loop 的 ready 事件 + ctx.on('ready', () => { + console.log('[hello-plugin] 插件已加载!') + }) +} +``` + +## 注册到 cordis.yml + +在你的 `cordis.yml` 中添加一条: + +```yaml +- id: hello + name: './src/my-plugin.ts' +``` + +启动后你会在控制台看到 `[hello-plugin] 插件已加载!`。 + +## 自动清理 + +通过 `ctx` 注册的任何东西——事件监听、tool、定时器——在插件卸载时都会被自动清理。你不需要手动 removeListener 或 clearInterval。 + +如果你有需要手动清理的资源(比如一个网络连接),用 `ctx.effect()` 告诉框架怎么清理: + +```typescript +export function apply(ctx: Context) { + ctx.effect(() => { + const timer = setInterval(() => { + console.log('heartbeat') + }, 5000) + + // 返回的函数会在插件卸载时被调用 + return () => clearInterval(timer) + }) +} +``` + +## 声明依赖 + +如果你的插件需要使用其他服务(如 `tools`、`llm`),需要声明 `inject`: + +```typescript +export const name = 'my-tool-plugin' +export const inject = ['tools'] + +export function apply(ctx: Context) { + // ctx.tools 现在可用 + ctx.tools.register(/* ... */) +} +``` + +框架会确保依赖的服务就绪后才加载你的插件。 + +## 插件的三种形态 + +除了函数形式,插件还支持对象形式和类形式: + +### 对象形式 + +```typescript +export default { + name: 'my-plugin', + inject: ['tools'], + apply(ctx: Context) { + // ... + }, +} +``` + +### 类形式 + +```typescript +import { Service } from 'cordis' + +export default class MyService extends Service { + static inject = ['tools'] + + constructor(ctx: Context) { + super(ctx, 'myService') + } + + start() { + // 服务启动逻辑 + } +} +``` + +大多数情况下,函数形式足够了。类形式用于需要对外提供服务的插件(见 [服务与依赖](../framework/service))。 + +## 完整示例 + +参考仓库中的 `examples/echo-agent/src/echo-tool.ts`,这是一个注册 tool 的插件: + +```typescript +import type { Context } from 'cordis' +import { defineTool } from '@deepseek-ai/dsh-tools' + +export const name = 'echo-tool' +export const inject = ['tools'] + +export function apply(ctx: Context) { + ctx.tools.register(defineTool({ + name: 'echo', + description: 'Echo the given text back, uppercased.', + parameters: { + text: { type: 'string', required: true }, + }, + async execute(args) { + return [{ type: 'text', text: `ECHO: ${args.text.toUpperCase()}` }] + }, + })) +} +``` + +## 下一步 + +- [开发一个 Tool](./tool) — 详细了解 tool 定义 DSL +- [插件配置](./config) — 让插件接受用户配置 diff --git a/website/zh-CN/develop/basic/tool.md b/website/zh-CN/develop/basic/tool.md new file mode 100644 index 0000000000..96d58da78d --- /dev/null +++ b/website/zh-CN/develop/basic/tool.md @@ -0,0 +1,199 @@ +# 开发一个 Tool + +Tool 是模型可以调用的能力。本文介绍如何用 `defineTool` 编写一个 tool。 + +## 最小示例 + +```typescript +import type { Context } from 'cordis' +import { defineTool } from '@deepseek-ai/dsh-tools' + +export const name = 'my-tool' +export const inject = ['tools'] + +export function apply(ctx: Context) { + ctx.tools.register(defineTool({ + name: 'greet', + description: 'Greet someone by name.', + parameters: { + name: { type: 'string', required: true, description: 'The name to greet' }, + }, + async execute(args) { + // args 自动推导为 { name: string } + return [{ type: 'text', text: `Hello, ${args.name}!` }] + }, + })) +} +``` + +## 参数定义 + +`parameters` 用一种简洁的格式描述参数,框架会自动转换为模型需要的 JSON Schema。 + +### 基本类型 + +```typescript +parameters: { + path: { type: 'string', required: true }, + limit: { type: 'number' }, + recursive: { type: 'boolean' }, +} +// 推导类型: { path: string; limit?: number; recursive?: boolean } +``` + +### 枚举 + +```typescript +parameters: { + mode: { type: 'string', required: true, enum: ['read', 'write', 'append'] }, +} +// 推导类型: { mode: string } (运行时校验 enum 值) +``` + +### 嵌套对象 + +```typescript +parameters: { + options: { + type: 'object', + properties: { + timeout: { type: 'number' }, + retries: { type: 'number' }, + }, + }, +} +// 推导类型: { options?: { timeout?: number; retries?: number } } +``` + +### 数组 + +```typescript +parameters: { + tags: { + type: 'array', + items: { type: 'string' }, + }, +} +// 推导类型: { tags?: string[] } +``` + +### 每个属性的字段 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `type` | `'string' \| 'number' \| 'boolean' \| 'object' \| 'array'` | 值类型 | +| `required` | `true` | 标记为必填(影响类型推导) | +| `description` | `string` | 发送给模型的描述 | +| `enum` | `string[]` | 允许的枚举值 | +| `properties` | `SchemaSpec` | 嵌套属性(type 为 object 时) | +| `items` | `SchemaProp` | 数组元素 schema(type 为 array 时) | + +## execute 函数 + +`execute` 接收经过校验的 `args`(类型自动推导)和一个 `exec` 上下文对象: + +```typescript +async execute(args, exec) { + // args: 根据 parameters 自动推导的类型 + // exec: ToolExecution 对象,提供执行上下文 + + // 返回 ContentBlock 数组 + return [{ type: 'text', text: 'result here' }] +} +``` + +### 返回值 + +`execute` 必须返回一个 `ContentBlock[]`,告诉模型 tool 的执行结果: + +```typescript +// 文本结果 +return [{ type: 'text', text: 'file content here...' }] + +// 多个 block +return [ + { type: 'text', text: 'Found 3 matches:' }, + { type: 'text', text: matchResults.join('\n') }, +] +``` + +### 参数校验 + +`defineTool` 在调用 `execute` 之前会自动校验模型生成的参数。如果参数不合法,会抛出 `ToolArgsError`,框架将其转换为 `isError` 结果返回给模型,让模型自行修正。 + +你不需要在 `execute` 里手动校验参数类型。 + +## 展示层 (Presentation) + +Tool 可以定义 UI 渲染方法,用于在终端或 ACP 客户端中展示 tool call 和 result: + +```typescript +defineTool({ + name: 'bash', + // ... + presentCall(args) { + return { + intent: 'terminal', + title: `bash(${JSON.stringify(args.command).slice(0, 60)})`, + } + }, + presentResult(args, result) { + return { + intent: 'terminal', + body: result.content.map(b => b.type === 'text' ? b.text : '').join(''), + } + }, +}) +``` + +`presentCall` 和 `presentResult` 是**纯函数**,不能有副作用——UI 可能在流式传输中和会话回放中多次调用它们。 + +## 注册与卸载 + +`ctx.tools.register()` 返回值就是 disposer。但由于你在 `ctx` 上调用,框架已经自动追踪了这个注册——插件卸载时会自动移除 tool。你不需要手动调用 disposer。 + +```typescript +// 这样就够了: +ctx.tools.register(defineTool({ /* ... */ })) + +// 不需要: +// const dispose = ctx.tools.register(...) +// ctx.on('dispose', dispose) +``` + +## 完整实战示例 + +一个文件计数 tool: + +```typescript +import type { Context } from 'cordis' +import { defineTool } from '@deepseek-ai/dsh-tools' +import { readdir } from 'node:fs/promises' + +export const name = 'file-counter' +export const inject = ['tools'] + +export function apply(ctx: Context) { + ctx.tools.register(defineTool({ + name: 'count_files', + description: 'Count files in a directory.', + parameters: { + path: { type: 'string', required: true, description: 'Directory path' }, + extension: { type: 'string', description: 'Filter by extension (e.g. ".ts")' }, + }, + async execute(args) { + const entries = await readdir(args.path, { withFileTypes: true }) + let files = entries.filter(e => e.isFile()) + if (args.extension) { + files = files.filter(f => f.name.endsWith(args.extension!)) + } + return [{ type: 'text', text: `Found ${files.length} files.` }] + }, + })) +} +``` + +## 下一步 + +- [插件配置](./config) — 让你的 tool 可配置 +- [能力三件套](../practice/) — 了解 seam/impl/consumer 模式 diff --git a/website/zh-CN/develop/framework/events.md b/website/zh-CN/develop/framework/events.md new file mode 100644 index 0000000000..0546fd68e7 --- /dev/null +++ b/website/zh-CN/develop/framework/events.md @@ -0,0 +1,152 @@ +# 事件系统 + +事件是 Cordis 插件间通信的核心机制。Harness 大量使用事件来实现松耦合的扩展点。 + +## 基本用法 + +### 监听事件 + +```typescript +ctx.on('event-name', (payload) => { + // 处理事件 +}) +``` + +### 触发事件 + +```typescript +ctx.emit('event-name', payload) +``` + +## 事件模式 + +Cordis 提供多种事件触发模式,适用于不同场景: + +### emit — 广播 + +所有监听器并行执行,不关心返回值: + +```typescript +// 触发 +ctx.emit('agent/turn-end', { agentId, turnIndex }) + +// 监听 +ctx.on('agent/turn-end', ({ agentId, turnIndex }) => { + console.log(`Turn ${turnIndex} ended`) +}) +``` + +### bail — 短路 + +依次调用监听器,第一个返回非 `undefined` 值的结果作为最终值: + +```typescript +// 触发 +const result = ctx.bail('some-check', input) + +// 监听(返回值阻止后续监听器) +ctx.on('some-check', (input) => { + if (shouldBlock(input)) return 'blocked' + // 返回 undefined 继续传递给下一个监听器 +}) +``` + +### serial — 顺序执行 + +所有监听器按注册顺序依次执行(异步安全): + +```typescript +await ctx.serial('setup-phase', context) +``` + +### waterfall — 管道 + +每个监听器接收前一个的输出,形成数据管道。**必须调用 `next()` 传递给下游**,不调用即为否决: + +```typescript +// 触发 +const finalMessages = await ctx.waterfall('llm/pre-request', messages) + +// 监听(必须调用 next) +ctx.on('llm/pre-request', async (messages, next) => { + // 可以修改 messages + messages.push(extraMessage) + // 必须调用 next() 传递给下一个监听器 + return next(messages) +}) +``` + +::: warning +Waterfall 监听器**必须调用 `next()`**。不调用 `next` 等于否决整个管道,这是故意为之的设计——用于实现拦截/网关逻辑。 +::: + +## Typed Events + +Harness 使用 TypeScript 声明合并来为事件提供类型安全: + +```typescript +declare module 'cordis' { + interface Events { + 'my-plugin/ready': (payload: { id: string }) => void + 'my-plugin/check': (input: string) => boolean | undefined + } +} + +// 现在 ctx.on('my-plugin/ready', ...) 和 ctx.emit('my-plugin/ready', ...) +// 都有正确的类型推导 +``` + +## 命名约定 + +Harness 事件遵循 `namespace/action` 命名: + +``` +agent/pre-step — agent 执行一步之前 +agent/post-step — agent 执行一步之后 +tool/call — tool 被调用 +tool/result — tool 返回结果 +llm/pre-request — LLM 请求发送前 +session/event — 会话事件被记录 +compact/start — 压缩开始 +compact/end — 压缩结束 +``` + +## 事件也是效果 + +通过 `ctx.on()` 注册的监听器会在插件卸载时自动移除: + +```typescript +export function apply(ctx: Context) { + // 这个监听器在插件 dispose 时自动清理 + ctx.on('agent/turn-end', handler) +} +``` + +## 实战示例:日志插件 + +一个记录所有 tool 调用的简单插件: + +```typescript +import type { Context } from 'cordis' + +export const name = 'tool-logger' + +export function apply(ctx: Context) { + ctx.on('tool/call', ({ name, args }) => { + console.log(`[tool] ${name}(${JSON.stringify(args)})`) + }) + + ctx.on('tool/result', ({ name, result }) => { + const text = result.content + .filter(b => b.type === 'text') + .map(b => b.text) + .join('') + console.log(`[tool result] ${text.slice(0, 100)}`) + }) +} +``` + +## 下一步 + +- [能力三件套](../practice/) — 事件在 capability seam 中的角色 +- [LLM 适配器](../practice/llm-adapter) — 实现一个完整的 LLM 后端 diff --git a/website/zh-CN/develop/framework/index.md b/website/zh-CN/develop/framework/index.md new file mode 100644 index 0000000000..8d2f7c2b8a --- /dev/null +++ b/website/zh-CN/develop/framework/index.md @@ -0,0 +1,139 @@ +# 插件与生命周期 + +深入了解 Cordis 插件模型和生命周期状态机。 + +## Fiber 状态机 + +每个被加载的插件对应一个 **Fiber**(作用域)。Fiber 有以下状态: + +``` +PENDING → LOADING → ACTIVE + ↘ FAILED +ACTIVE → UNLOADING → DISPOSED +``` + +| 状态 | 含义 | +|------|------| +| PENDING | 已声明但依赖未就绪 | +| LOADING | 依赖就绪,正在执行 `apply` | +| ACTIVE | 插件运行中 | +| FAILED | `apply` 抛出异常 | +| UNLOADING | 正在卸载,清理中 | +| DISPOSED | 已完全卸载 | + +## 依赖驱动的加载 + +声明了 `inject` 的插件不会立即加载,而是等待依赖的服务就绪: + +```typescript +export const inject = ['tools', 'llm'] + +export function apply(ctx: Context) { + // 到这里时,ctx.tools 和 ctx.llm 一定存在 +} +``` + +如果依赖的服务消失(比如提供者被热替换),插件会被自动卸载(ACTIVE → DISPOSED),待服务恢复后重新加载。 + +## 自动清理机制 + +通过 `ctx` 做的任何注册,在插件卸载时都会自动撤销: + +```typescript +export function apply(ctx: Context) { + // 事件监听——卸载时自动移除 + ctx.on('some-event', handler) + + // 自定义资源——卸载时调用返回的函数 + ctx.effect(() => { + const connection = createConnection() + return () => connection.close() + }) +} +``` + +以下操作都会被自动追踪和清理: +- `ctx.on(event, handler)` — 事件监听 +- `ctx.tools.register(tool)` — tool 注册 +- `ctx.llm.registerAdapter(names, adapter)` — LLM 适配器注册 +- `ctx.effect(() => cleanup)` — 自定义资源 + +插件卸载时,这些注册按倒序逐个撤销。 + +## 嵌套上下文 + +`ctx.plugin()` 创建子 Fiber,它继承父上下文但有独立的生命周期: + +```typescript +export function apply(ctx: Context) { + // 注册一个子插件 + ctx.plugin(childPlugin) + + // 子插件有自己的 Fiber,父卸载时子也卸载 +} +``` + +## dispose 语义 + +当你需要提前终止一个插件实例: + +```typescript +const fiber = ctx.plugin(myPlugin) + +// 之后可以手动 dispose +fiber.dispose() +``` + +`dispose` 保证: +1. 该插件注册的所有东西被撤销 +2. 它的子插件也被递归卸载 +3. 所有异步清理完成后 Promise resolve + +## 热替换 (HMR) + +在开发环境中(`cordis.yml` 加载了 `@cordisjs/plugin-hmr`),修改插件源文件会自动触发: + +1. 卸载旧插件(清理所有注册) +2. 重新加载新代码 +3. 执行新的 `apply` + +因为所有注册都会被自动清理,所以热替换天然安全——不会留下旧状态。 + +## 实战:理解生命周期 + +```typescript +export function apply(ctx: Context) { + console.log('plugin loading') + + ctx.on('ready', () => { + console.log('context ready') + }) + + ctx.on('dispose', () => { + console.log('plugin disposing') + }) + + ctx.effect(() => { + console.log('effect registered') + return () => console.log('effect cleaned up') + }) +} +``` + +加载时输出: +``` +plugin loading +effect registered +context ready +``` + +卸载时输出(逆序): +``` +plugin disposing +effect cleaned up +``` + +## 下一步 + +- [服务与依赖](./service) — 让你的插件对外提供能力 +- [事件系统](./events) — 插件间通信的核心机制 diff --git a/website/zh-CN/develop/framework/service.md b/website/zh-CN/develop/framework/service.md new file mode 100644 index 0000000000..08d9a1b2c8 --- /dev/null +++ b/website/zh-CN/develop/framework/service.md @@ -0,0 +1,147 @@ +# 服务与依赖 + +服务 (Service) 是插件对外暴露能力的方式。依赖 (inject) 是插件声明自己需要哪些服务。 + +## 什么是服务 + +在 Harness 中,`tools`、`llm`、`agents` 都是服务。服务是挂载在 `ctx` 上的命名能力: + +```typescript +ctx.tools // ToolRegistry 服务 +ctx.llm // LLM 服务 +ctx.agents // Agent 服务 +``` + +任何插件都可以提供一个新服务,供其他插件使用。 + +## 使用服务 + +声明 `inject` 来使用已有服务: + +```typescript +export const inject = ['tools'] + +export function apply(ctx: Context) { + // ctx.tools 在这里一定存在且就绪 + ctx.tools.register(/* ... */) +} +``` + +框架保证:在 `apply` 执行时,`inject` 声明的服务已经全部就绪。如果服务还没准备好,你的插件会等着,不会执行。 + +## 提供服务 + +### 使用 Service 基类 + +```typescript +import { Service, type Context } from 'cordis' + +export default class MetricsService extends Service { + static inject = ['llm'] // 本服务也可以依赖其他服务 + + constructor(ctx: Context) { + super(ctx, 'metrics') // 'metrics' 是服务名 + } + + // 服务的公开方法 + record(event: string, value: number) { + // ... + } +} +``` + +加载这个插件后,其他插件就可以通过 `ctx.metrics` 访问它: + +```typescript +export const inject = ['metrics'] + +export function apply(ctx: Context) { + ctx.metrics.record('tool_call', 1) +} +``` + +### 类型声明 + +使用 TypeScript 声明合并让 `ctx.metrics` 有正确类型: + +```typescript +import { Service, type Context } from 'cordis' + +declare module 'cordis' { + interface Context { + metrics: MetricsService + } +} + +export default class MetricsService extends Service { + constructor(ctx: Context) { + super(ctx, 'metrics') + } + + record(event: string, value: number) { /* ... */ } +} +``` + +## 依赖的行为 + +### 必选依赖 vs 可选依赖 + +```typescript +// 必选:服务不存在时,插件不会加载 +export const inject = ['tools'] + +// 可选:服务不存在时,插件仍然加载,但 ctx.xxx 可能是 undefined +export const inject = { optional: ['metrics'] } +``` + +### 服务消失时的行为 + +如果一个必选依赖的服务在运行时消失(比如提供者被卸载): + +1. 依赖它的插件自动 dispose +2. 当服务重新出现时,插件自动重新加载 + +这保证了不会出现"调用一个已不存在的服务"的情况。 + +## 服务隔离 + +`cordis.yml` 支持服务隔离——同一个服务可以有多个实例,不同插件组看到不同实例: + +```yaml +- id: group-a + name: 'group:' + config: + - name: '@deepseek-ai/dsh-bash-local' + config: + timeoutMs: 5000 + - name: './src/plugin-a.ts' + +- id: group-b + name: 'group:' + config: + - name: '@deepseek-ai/dsh-bash-local' + config: + timeoutMs: 60000 + - name: './src/plugin-b.ts' +``` + +`plugin-a` 和 `plugin-b` 各自看到自己组内的 bash 实例,互不影响。 + +## Harness 内置服务一览 + +| 服务名 | 提供者 | 用途 | +|--------|--------|------| +| `tools` | dsh-tools | Tool 注册表 | +| `llm` | dsh-llm | LLM 调用 + 适配器注册 | +| `agents` | dsh-agent | Agent 实例管理 | +| `session` | dsh-session | 会话事件流 | +| `systemPrompt` | dsh-system-prompt | 系统提示词组装 | +| `bash` | dsh-bash-local | Bash 命令执行 | +| `fs` | dsh-fs-local | 文件系统操作 | +| `subagent` | dsh-subagent | 子代理委派 | +| `persistence` | dsh-session-persistence | 会话持久化 | + +## 下一步 + +- [事件系统](./events) — 插件间松耦合通信 +- [能力三件套](../practice/) — 服务在 seam 模式中的应用 diff --git a/website/zh-CN/develop/practice/index.md b/website/zh-CN/develop/practice/index.md new file mode 100644 index 0000000000..dd0ec1cb60 --- /dev/null +++ b/website/zh-CN/develop/practice/index.md @@ -0,0 +1,156 @@ +# 能力的三层拆分 + +当一个能力(插件)足够通用(比如"执行 bash 命令"),Harness 会把它拆成三个包:**接口**、**实现**、**消费者**。这样可以独立替换其中任何一层。 + +## 以 Bash 为例 + +考虑 "Bash 执行" 这个能力: + +- **接口** (`dsh-bash`) — 定义"bash 执行"长什么样:输入是什么、输出是什么 +- **实现** (`dsh-bash-local`) — 真正在本地跑命令的代码 +- **消费者** (`dsh-tool-bash`) — 把这个能力包装成模型能调用的 tool + +``` +┌─────────────┐ ┌──────────────────┐ ┌──────────────┐ +│ dsh-bash │────▶│ dsh-bash-local │ │ dsh-tool-bash│ +│ (接口) │ │ (实现) │ │ (消费者/tool)│ +└─────────────┘ └──────────────────┘ └──────────────┘ + ▲ │ + └────────────────────────────────────────────┘ + inject: ['bash'] +``` + +## 拆分的好处 + +### 具体实现可替换 + +同一个接口可以有多种实现。用户通过 `cordis.yml` 选择: + +```yaml +# 本地执行 +- name: '@deepseek-ai/dsh-bash-local' + +# 或:远程沙箱执行(未来) +# - name: '@deepseek-ai/dsh-bash-remote' +# config: +# endpoint: 'https://sandbox.example.com' +``` + +接口不变、tool 不变,只换实现。 + +### 独立演进 + +- 接口定义稳定后很少改动 +- 实现可以独立优化(性能、安全) +- 消费者(tool)可以调整对模型的呈现方式 + +### 依赖解耦 + +- 实现 depend on 接口 +- 消费者 depend on 接口 +- 实现和消费者**互不依赖** + +## Harness 中内置的三件套 + +| 能力 | 接口 (seam) | 实现 | 消费者 (tool) | +|------|-------------|------|---------------| +| Bash | `dsh-bash` | `dsh-bash-local` | `dsh-tool-bash` | +| 文件系统 | `dsh-fs` | `dsh-fs-local` + `dsh-fs-policy` | `dsh-tool-fs` | +| Web | `dsh-web` | `dsh-web-fetch-local` / `dsh-web-search-*` | `dsh-tool-web` | +| 子代理 | `dsh-subagent` | `dsh-subagent-spawn` / `dsh-subagent-fork` | `dsh-tool-subagent` | +| 压缩 | `dsh-compact` | `dsh-compact-basic` | (内置于 agent-loop) | + +## 开发你自己的三件套 + +### 第一步:定义接口 + +```typescript +// packages/my-cap/my-cap/src/index.ts +import { Service, type Context } from 'cordis' + +declare module 'cordis' { + interface Context { + myCap: MyCapService + } +} + +export abstract class MyCapService extends Service { + constructor(ctx: Context) { + super(ctx, 'myCap') + } + + /** 执行能力的核心方法 */ + abstract execute(request: MyCapRequest): Promise +} + +export interface MyCapRequest { + input: string +} + +export interface MyCapResult { + output: string +} +``` + +### 第二步:编写实现 + +```typescript +// packages/my-cap/my-cap-local/src/index.ts +import type { Context } from 'cordis' +import { MyCapService, type MyCapRequest, type MyCapResult } from '@deepseek-ai/dsh-my-cap' + +class MyCapLocal extends MyCapService { + async execute(request: MyCapRequest): Promise { + // 具体实现 + return { output: request.input.toUpperCase() } + } +} + +export const name = 'my-cap-local' + +export function apply(ctx: Context) { + ctx.plugin(MyCapLocal) +} +``` + +### 第三步:编写消费者 (tool) + +```typescript +// packages/my-cap/tool-my-cap/src/index.ts +import type { Context } from 'cordis' +import { defineTool } from '@deepseek-ai/dsh-tools' + +export const name = 'tool-my-cap' +export const inject = ['tools', 'myCap'] + +export function apply(ctx: Context) { + ctx.tools.register(defineTool({ + name: 'my_cap', + description: 'Execute my capability.', + parameters: { + input: { type: 'string', required: true }, + }, + async execute(args) { + const result = await ctx.myCap.execute({ input: args.input }) + return [{ type: 'text', text: result.output }] + }, + })) +} +``` + +### 在 cordis.yml 中组合 + +```yaml +- name: '@deepseek-ai/dsh-my-cap-local' +- name: '@deepseek-ai/dsh-tool-my-cap' +``` + +## 设计要点 + +- **不要预防性拆分** — 只有当你确实需要可替换实现时才拆三件套。一个简单的 tool 插件不需要拆分。 +- **接口定义 Request/Result 类型** — 实现和消费者只依赖接口包。 +- **Explicit > Implicit** — 实现中的默认值处理应该是显式的 `resolve(request): Spec` 步骤,不是隐藏在 `run()` 中的 `?? default`。 + +## 下一步 + +- [LLM 适配器](./llm-adapter) — 实现一个 LLM 后端(最常见的 seam 扩展) diff --git a/website/zh-CN/develop/practice/llm-adapter.md b/website/zh-CN/develop/practice/llm-adapter.md new file mode 100644 index 0000000000..20b1fa2c88 --- /dev/null +++ b/website/zh-CN/develop/practice/llm-adapter.md @@ -0,0 +1,169 @@ +# LLM 适配器 + +本文介绍如何为 Harness 接入一个新的 LLM 提供方。 + +## 概述 + +LLM 适配器是一个继承 `LlmAdapter` 的类,实现 `stream()` 方法,将 Harness 的统一请求格式转换为具体 API 的调用。 + +## 最小实现 + +```typescript +import type { Context } from 'cordis' +import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm' + +class MyAdapter extends LlmAdapter { + private apiKey: string + + constructor(apiKey: string) { + super() + this.apiKey = apiKey + } + + async *stream(options: GenerateOptions): AsyncIterable { + // 1. 将 options.messages 转换为你的 API 格式 + // 2. 调用 API(流式) + // 3. 将 API 响应转换为 StreamChunk 序列 + } +} + +export interface Config { + apiKey: string + models: string[] +} + +export const name = 'my-llm-adapter' +export const inject = ['llm'] + +export function apply(ctx: Context, config: Config) { + const adapter = new MyAdapter(config.apiKey) + ctx.llm.registerAdapter(config.models, adapter) +} +``` + +## StreamChunk 协议 + +`stream()` 必须按以下协议 yield chunk: + +```typescript +// 1. 每个内容块以 block-start 开始 +yield { type: 'block-start', index: 0, blockType: 'text' } + +// 2. 文本块使用 text-delta +yield { type: 'text-delta', index: 0, text: 'Hello' } +yield { type: 'text-delta', index: 0, text: ' world' } + +// 3. 每个内容块以 block-end 结束(携带完整 block) +yield { + type: 'block-end', + index: 0, + block: { type: 'text', text: 'Hello world' }, +} + +// 4. Tool call 块 +yield { type: 'block-start', index: 1, blockType: 'tool-call' } +yield { + type: 'tool-call-delta', + index: 1, + id: CallId('call-123'), + name: 'bash', + argumentsDelta: '{"command":"ls"}', +} +yield { + type: 'block-end', + index: 1, + block: { + type: 'tool-call', + id: CallId('call-123'), + name: 'bash', + arguments: '{"command":"ls"}', + }, +} + +// 5. Token 用量 +yield { type: 'usage', usage: { inputTokens: 100, outputTokens: 50 } } + +// 6. 结束原因 +yield { type: 'finish', reason: { kind: 'stop' } } +// 或: { kind: 'tool-calls' } 表示模型想调用 tool +``` + +### 关键规则 + +- 每个 `block-start` 必须有对应的 `block-end` +- `index` 从 0 递增,标识内容块顺序 +- `tool-call-delta` 的 `argumentsDelta` 是 JSON 字符串的增量(可以一次 yield 全部,也可以分多次) +- `finish` 必须是最后一个 chunk +- `usage` 在 `finish` 之前 yield + +## GenerateOptions + +`stream()` 接收的请求包含: + +```typescript +interface GenerateOptions { + /** 模型名 */ + model: string + /** 对话历史 */ + messages: Message[] + /** 可用的 tool 列表 */ + tools?: ToolSpec[] + /** 系统提示词 */ + system?: string + /** 最大输出 token */ + maxTokens?: number + /** 温度 */ + temperature?: number +} +``` + +你的适配器需要将这些映射到具体 API 的参数。 + +## 注册适配器 + +```typescript +ctx.llm.registerAdapter(['model-name-1', 'model-name-2'], adapter) +``` + +第一个参数是该适配器支持的模型名列表。当用户在 `cordis.yml` 中配置 `model: model-name-1` 时,框架会路由到这个适配器。 + +## 在 cordis.yml 中使用 + +```yaml +- id: my-llm + name: './src/my-llm-adapter.ts' + config: + apiKey: !!js process.env.MY_API_KEY + models: + - my-model-v1 + - my-model-v2 + +- id: stdio-agent + name: '@deepseek-ai/dsh-stdio-agent' + config: + model: my-model-v1 # 引用上面注册的模型名 +``` + +## 实战参考 + +仓库中有两个完整实现可供参考: + +- `packages/llm/llm-deepseek/` — DeepSeek API 适配器(OpenAI 兼容格式) +- `packages/llm/llm-pi-ai/` — Pi AI 适配器(不同的 API 格式) +- `examples/echo-agent/src/mock-llm.ts` — 最简 mock 适配器(教学用) + +mock 适配器是学习 StreamChunk 协议的最佳起点——它用纯本地逻辑演示了完整的 chunk 序列。 + +## 错误处理 + +适配器中的异常会被 agent-loop 捕获并转化为 `LlmError`,告知上层。不需要在 `stream()` 内部做错误恢复——让异常冒泡即可。 + +```typescript +async *stream(options: GenerateOptions): AsyncIterable { + const response = await fetch(this.endpoint, { /* ... */ }) + if (!response.ok) { + throw new Error(`API error: ${response.status}`) + } + // ... 正常流式处理 +} +``` diff --git a/website/zh-CN/guide/config.md b/website/zh-CN/guide/config.md new file mode 100644 index 0000000000..d555a0a478 --- /dev/null +++ b/website/zh-CN/guide/config.md @@ -0,0 +1,342 @@ +# 配置文件 + +Harness 使用 `cordis.yml` 描述一个 Agent 加载哪些插件、以什么参数运行。 + +## 从例子开始 + +### echo-agent 的配置 + +这是一开始的第一个 Agent 的完整配置: + +```yaml +# 热替换:修改代码后自动重载,不用手动重启 +- id: hmr + name: '@cordisjs/plugin-hmr' + config: + root: ['.'] + +# Mock 模型:从本地 `.ts` 文件加载,注册一个名为 `mock-llm` 的工具 +# 本地模拟 LLM 响应,不联网 +- id: mock-llm + name: './src/mock-llm.ts' + +# Echo 工具:收到文本后转大写返回 +- id: echo-tool + name: './src/echo-tool.ts' + +# Bash 执行器:从 npm 包 `@deepseek-ai/dsh-bash-local`加载,提供 bash 命令执行能力 +- id: bash + name: '@deepseek-ai/dsh-bash-local' + +# 应用主体:把 session 管理、tool 调度、agent loop 等组装成一个可交互的终端 Agent +# 只需告诉它用哪个模型 (`model`)、什么人设 (`persona`) +- id: stdio-agent + name: '@deepseek-ai/dsh-stdio-agent' + config: + model: mock-echo + persona: 'You are echo-agent, a demo agent.' + welcome: 'echo-agent ready. Type a message ("echo " triggers the tool).' + persistenceRoot: './.sessions' +``` + +### coding-agent 的配置 + +真实场景——接入 DeepSeek API,带完整工具链: + +```yaml +# 热替换:同上,开发时自动重载 +- id: hmr + name: '@cordisjs/plugin-hmr' + config: + root: ['.'] + +# LLM 后端:从 npm 包加载,具备接入 DeepSeek API 能力 +# `!!js` 从环境变量读取密钥,不会写进配置文件 +# `models` 声明该适配器能处理哪些模型名 +- id: llm-deepseek + name: '@deepseek-ai/dsh-llm-deepseek' + config: + apiKey: !!js process.env.DEEPSEEK_API_KEY + baseURL: !!js process.env.DEEPSEEK_BASE_URL + models: + - deepseek-v4-pro + - deepseek-v4-flash + +# Bash 执行器:让 Agent 能跑 shell 命令 +# timeoutMs 设置单条命令的超时时间 +- id: bash + name: '@deepseek-ai/dsh-bash-local' + config: + timeoutMs: 60000 + +# 应用主体:和 echo-agent 一样的框架,只是配置不同 +# `model` 指定默认使用哪个模型(要和上面 models 列表里的名字对应) +# `persona` 是系统提示词,{{model}} 会被替换为实际模型名 +# `resumeSessionId` 设了就恢复旧对话,没设就每次新建 +- id: stdio-agent + name: '@deepseek-ai/dsh-stdio-agent' + config: + model: deepseek-v4-flash + resumeSessionId: !!js process.env.RESUME_SESSION_ID + persistenceRoot: './.sessions' + welcome: 'agent REPL ready. Give it a coding task.' + persona: | + You are coding-agent, a coding assistant powered by the {{model}} model. + Verify your work by running the code or tests. Keep answers brief and factual. + +# 自动压缩:对话太长时自动总结旧内容,腾出上下文空间 +# contextWindow 是模型能看到的 token 上限 +# thresholdRatio 超过这个比例就触发压缩 +- id: compact-basic + name: '@deepseek-ai/dsh-compact-basic' + config: + contextWindow: 128000 + thresholdRatio: 0.8 + retainTokens: 20480 + maxTokens: 8192 + +# 子代理:把子任务分配给独立的 Agent 去做 +# subagent 是服务注册,spawn/fork 是两种委派方式: +# spawn — 全新子代理,不知道父级在聊什么 +# fork — 继承父级对话上下文的子代理 +# tool-subagent 把委派能力暴露给模型,toolName 是模型看到的工具名 +- id: subagent + name: '@deepseek-ai/dsh-subagent' + +- id: subagent-spawn + name: '@deepseek-ai/dsh-subagent-spawn' + config: + providerName: spawn + +- id: subagent-fork + name: '@deepseek-ai/dsh-subagent-fork' + config: + providerName: fork + +- id: tool-subagent + name: '@deepseek-ai/dsh-tool-subagent' + config: + provider: spawn + toolName: subagent + +- id: tool-subagent-fork + name: '@deepseek-ai/dsh-tool-subagent' + config: + provider: fork + toolName: subagent_fork + +# 任务追踪:模型可以用 todo_write 记录和更新任务清单 +- id: tool-todo + name: '@deepseek-ai/dsh-tool-todo' + +# 文件系统:让 Agent 能读写编辑文件 +# fs-local 提供本地文件操作能力,cwd 是工作目录 +# fs-policy 是安全策略——必须先读才能写,防止模型盲写 +# tool-fs 把能力暴露给模型(read / write / edit 三个工具) +- id: fs-local + name: '@deepseek-ai/dsh-fs-local' + config: + cwd: !!js process.cwd() + +- id: fs-policy + name: '@deepseek-ai/dsh-fs-policy' + +- id: tool-fs + name: '@deepseek-ai/dsh-tool-fs' +``` + +和 echo-agent 对比:同一个 `dsh-stdio-agent` 应用主体,只是把 mock 换成了真实 API,加上了更多工具插件。 + +## 语法详解 + +### 插件声明字段 + +每个插件条目支持以下字段: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `name` | string | 是 | 插件来源(npm 包名或相对路径) | +| `id` | string | 否 | 实例标识符,用于日志和调试 | +| `config` | object | 否 | 传递给插件的配置 | +| `disabled` | boolean | 否 | 设为 `true` 临时禁用该插件 | + +### 插件来源 (`name`) + +**npm 包** — 已安装的 `@deepseek-ai/dsh-*` 包或第三方包: + +```yaml +- name: '@deepseek-ai/dsh-llm-deepseek' +``` + +**相对路径** — 本地 TypeScript 文件(相对于 `cordis.yml` 所在目录): + +```yaml +- name: './src/my-tool.ts' +``` + +### 环境变量 (`!!js`) + +用 `!!js` 标签在配置中引用运行时表达式: + +```yaml +config: + apiKey: !!js process.env.DEEPSEEK_API_KEY + cwd: !!js process.cwd() +``` + +::: warning +是 `!!js`(两个感叹号),不是 `!js`。写错了会静默失败。 +::: + +环境变量从仓库根目录的 `.env` 文件自动加载(已被 gitignore)。 + +### 禁用插件 + +不想删配置但暂时不加载?加一行 `disabled`: + +```yaml +- id: compact-basic + name: '@deepseek-ai/dsh-compact-basic' + disabled: true + config: + contextWindow: 128000 +``` + +## 各插件配置参考 + +### stdio-agent(标准应用主体) + +**包名:** `@deepseek-ai/dsh-stdio-agent` + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `model` | string | **必填** | 使用的模型名,需与 LLM 适配器注册的名字一致 | +| `persona` | string | `''` | 系统提示词。支持 `{{model}}` 等模板变量 | +| `toolOrder` | string[] | — | 模型看到的工具顺序。省略则按字母排序 | +| `persistenceRoot` | string | `'./.sessions'` | 会话日志存储目录 | +| `welcome` | string | `'ready.'` | 启动时显示的欢迎信息 | +| `resumeSessionId` | string | — | 恢复指定会话 ID。留空则每次新建 | + +### llm-deepseek(DeepSeek 适配器) + +**包名:** `@deepseek-ai/dsh-llm-deepseek` + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `apiKey` | string | `$DEEPSEEK_API_KEY` | API 密钥。省略则从环境变量读取 | +| `baseURL` | string | `$DEEPSEEK_BASE_URL` 或官方地址 | API 端点 | +| `models` | string[] | `['deepseek-v4-flash', 'deepseek-v4-pro']` | 注册的模型名列表 | +| `thinking` | `'enabled'` \| `'disabled'` | `'enabled'` | 是否开启思维链 | +| `reasoningEffort` | `'high'` \| `'max'` | — | 思维链深度(仅 thinking 开启时有效) | + +### bash-local(Bash 执行器) + +**包名:** `@deepseek-ai/dsh-bash-local` + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `cwd` | string | `process.cwd()` | 命令执行的工作目录 | +| `timeoutMs` | number | `120000` | 单条命令的超时时间(毫秒) | +| `maxTimeoutMs` | number | `600000` | 单条命令超时的上限(模型不能请求更久) | +| `maxOutputBytes` | number | `64000` | 单次输出的内存上限(超出后溢出到临时文件) | +| `graceMs` | number | `3000` | kill 时从 SIGTERM 到 SIGKILL 的等待时间 | + +### compact-basic(自动压缩) + +**包名:** `@deepseek-ai/dsh-compact-basic` + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `contextWindow` | number | **必填** | 模型的上下文窗口大小(token) | +| `thresholdRatio` | number | **必填** | token 占用超过此比例时触发压缩(0-1) | +| `retainTokens` | number | **必填** | 压缩后至少保留多少 token 的近期内容 | +| `maxTokens` | number | **必填** | 总结时的最大输出 token | +| `summarizationModel` | string | `''`(用当前模型) | 专门用于总结的模型名 | +| `compactionRetries` | number | **必填** | 首次压缩后仍超标时的额外重试次数 | +| `auto` | boolean | `true` | 是否自动在每步前检查并触发压缩 | +| `charsPerToken` | number | `4` | 每 token 估算字符数。中文应设 1-2 | + +### fs-local(文件系统) + +**包名:** `@deepseek-ai/dsh-fs-local` + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `cwd` | string | `process.cwd()` | 工作目录,相对路径以此为基准 | + +### fs-policy(文件系统策略) + +**包名:** `@deepseek-ai/dsh-fs-policy` + +无配置项。加载即启用"必须先读才能写"的安全策略。 + +### tool-fs(文件系统工具) + +**包名:** `@deepseek-ai/dsh-tool-fs` + +无配置项。加载后向模型暴露 `read`、`write`、`edit` 三个工具。 + +### tool-web(Web 工具) + +**包名:** `@deepseek-ai/dsh-tool-web` + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `search` | boolean | `true` | 是否注册 `web_search` 工具 | +| `fetch` | boolean | `true` | 是否注册 `web_fetch` 工具 | +| `searchMaxResults` | number | `8` | 单次搜索返回的最大结果数 | + +### subagent-spawn / subagent-fork(子代理后端) + +**包名:** `@deepseek-ai/dsh-subagent-spawn` / `@deepseek-ai/dsh-subagent-fork` + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `providerName` | string | `'spawn'` / `'fork'` | 注册到子代理服务的 provider 名称 | + +### tool-subagent(子代理工具) + +**包名:** `@deepseek-ai/dsh-tool-subagent` + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `provider` | string | **必填** | 使用哪个 provider(如 `spawn`、`fork`) | +| `toolName` | string | `'subagent'` | 暴露给模型的工具名。多次加载时必须不同 | +| `agentOptions.model` | string | — | 子代理使用的模型名(省略则继承父代理) | + +### tool-todo(任务清单) + +**包名:** `@deepseek-ai/dsh-tool-todo` + +无配置项。加载后向模型暴露 `todo_write` 工具。 + +### hmr(热替换) + +**包名:** `@cordisjs/plugin-hmr` + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `root` | string[] | **必填** | 监听文件变更的目录列表 | + +::: tip +hmr 仅用于开发环境。它需要 `node --expose-internals` 启动参数,`demo:*` 脚本已自动添加。 +::: + +--- + +## 加载顺序 + +`cordis.yml` 的顺序就是加载顺序。推荐: + +1. **hmr** — 热替换(仅开发时需要) +2. **LLM 适配器** — 模型后端 +3. **执行器** — bash、fs 等能力提供者 +4. **应用主体** — `dsh-stdio-agent` 或 `dsh-acp-agent` +5. **附加插件** — compact、subagent、todo 等 + +应用主体内部已经捆绑了核心能力(session、tools、agent-loop),不需要手动加载。 + +## 下一步 + +- [开发插件](../develop/basic/) — 编写自己的插件 +- [API 参考](../api/) — 查看各插件完整接口 diff --git a/website/zh-CN/guide/index.md b/website/zh-CN/guide/index.md new file mode 100644 index 0000000000..8b7211b308 --- /dev/null +++ b/website/zh-CN/guide/index.md @@ -0,0 +1,47 @@ +# 介绍 + +DeepSeek Harness 是一个**插件化的 Agent 开发框架**,基于 [Cordis](https://github.com/cordiverse/cordis) 微内核构建。它的核心理念是:**一切皆插件**。 + +## 它是什么 + +Harness 将一个 AI Agent(智能体) 所需要的所有能力——LLM 调用、工具执行、会话管理、子任务分配——全部构建为可组合的插件。你通过一个 `cordis.yml` 配置文件来声明加载哪些插件、使用什么参数,就能组装出一个完整的 Agent。 + +```yaml +# 选择 LLM 后端 +- name: '@deepseek-ai/dsh-llm-deepseek' + config: + apiKey: !!js process.env.DEEPSEEK_API_KEY + +# 选择应用模板 +- name: '@deepseek-ai/dsh-stdio-agent' + config: + model: deepseek-v4-flash +``` + +## 适合谁 + +### 应用使用者 + +如果你只是想用一个现成的 Agent 应用(如编程助手、对话代理),你需要的全部操作就是: + +1. 复制一个 example 模板 +2. 填写 API key +3. 运行 + +不需要写任何代码。详见 [快速开始](./quickstart)。 + +### 插件开发者 + +如果你想为 Agent 添加新能力——一个自定义 tool、一个新的 LLM 适配器、一个新的执行后端——你需要编写一个插件。Harness 提供了清晰的扩展接口和类型安全的开发体验。详见 [开发](../develop/basic/)。 + +## 核心特性 + +- **只需要配置** — `cordis.yml` 决定能力集合,换模型、加工具只需改一行 +- **随时替换 (HMR)** — 开发时修改插件代码,无需重启进程 + +## 技术栈 + +- **运行时**: Node.js >= 24 +- **语言**: TypeScript (ESM) +- **框架**: Cordis +- **包管理**: pnpm workspaces diff --git a/website/zh-CN/guide/quickstart.md b/website/zh-CN/guide/quickstart.md new file mode 100644 index 0000000000..f15ac182cf --- /dev/null +++ b/website/zh-CN/guide/quickstart.md @@ -0,0 +1,98 @@ +# 快速开始 + +本指南带你在 5 分钟内跑起一个 Agent。 + +## 环境准备 + +- [Node.js](https://nodejs.org/) >= 24 +- [pnpm](https://pnpm.io/) >= 9 + +```sh +# 确认版本 +node -v # v24.x 或更高 +pnpm -v # 9.x 或更高 +``` + +## 第一步:运行 echo-agent + +echo-agent 不需要 API key,装好依赖就能跑。 + +```sh +# 克隆仓库 +git clone https://github.com/deepseek-harness/deepseek-harness.git +cd deepseek-harness + +# 安装依赖 +pnpm install +# 如果看到 ERR_PNPM_IGNORED_BUILDS,可以忽略——安装已经成功了。 +# 想消除这个提示可以跑一次: pnpm approve-builds + +# 启动 echo-agent +pnpm run demo:echo +``` + +启动后你会看到: + +``` +echo-agent ready. Type a message ("echo " triggers the tool). +> +``` + +试着输入: + +``` +> echo hello world +``` + +你会看到模型发起了一次 tool call(工具调用),echo 工具将文本转为大写并返回: + +``` +[tool call] echo({"text":"hello world"}) +[tool result] ECHO: HELLO WORLD +``` + +恭喜!环境没问题。 + +## 第二步:使用真实模型调用 + +接下来接入真实的 DeepSeek 模型,跑一个完整的命令行 Agent。 + +### 获取 API Key + +前往 [DeepSeek Platform](https://platform.deepseek.com/) 获取你的 API key。 + +### 配置环境变量 + +在仓库根目录创建 `.env` 文件(已被 gitignore): + +```sh +DEEPSEEK_API_KEY=sk-your-key-here +``` + +### 启动 coding-agent + +```sh +pnpm run demo:repl +``` + +``` +agent REPL ready. Give it a coding task. +> +``` + +这就是一个完整的编程助手,它能读写文件、跑命令、拆分子任务。 + +试着给它一个任务: + +``` +> 在当前目录创建一个 hello.js,内容是打印 "Hello from Harness!",然后运行它 +``` + +## 回头看 + +echo-agent 和 coding-agent 用的是同一个应用框架(`@deepseek-ai/dsh-stdio-agent`),区别只在 `cordis.yml`——换了哪些插件、填了什么配置。你以后定制自己的 Agent 也是同样的方式。 + +## 下一步 + +- [配置文件](./config) — 了解 `cordis.yml` 的完整语法 +- [开发插件](../develop/basic/) — 编写你自己的 tool 或后端 diff --git a/website/zh-CN/index.md b/website/zh-CN/index.md new file mode 100644 index 0000000000..90b23e483a --- /dev/null +++ b/website/zh-CN/index.md @@ -0,0 +1,21 @@ +--- +layout: home +hero: + name: DeepSeek Harness + text: 插件化 Agent 开发框架 + tagline: 基于 Cordis 微内核,一切皆插件 + actions: + - theme: brand + text: 快速开始 + link: /zh-CN/guide/quickstart + - theme: alt + text: 开发插件 + link: /zh-CN/develop/basic/ +features: + - title: 插件化架构 + details: 基于 Cordis 效果系统,所有能力通过插件注册,加载即生效、卸载即还原。 + - title: 配置即组合 + details: 一个 cordis.yml 决定整个 Agent 的能力组合——换模型、加工具,只需改一行配置。 + - title: 开箱即用 + details: 内置 LLM 调用、文件读写、Bash 执行、子代理委派等完整工具链,复制模板即可运行。 +---