feat(docs): build maintainable documentation site

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

5
website/.gitignore vendored
View File

@@ -1,3 +1,4 @@
node_modules/
.vitepress/dist/
.vitepress/cache/
.cache/
.dist/
.generated/

View File

@@ -0,0 +1,142 @@
/** VitePress configuration for the locally projected documentation site. */
import type { DefaultTheme, PageData } from 'vitepress'
import type { ViteDevServer } from 'vite'
import { withMermaid } from 'vitepress-plugin-mermaid'
import { docsPages, type DocsPage } from '../docs.ts'
import { docsSourceFiles, projectDocs } from '../../scripts/project-doc-site.ts'
projectDocs()
const sectionOrder = [
'入门',
'基础',
'框架能力',
'实战',
'Concepts',
'Generated reference',
'Data structures',
'Cookbook',
]
function sidebar(collection: DocsPage['sidebar']): DefaultTheme.SidebarItem[] {
const pages = docsPages.filter(page => page.sidebar === collection && page.route !== 'index.md')
const sections = new Map<string, DocsPage[]>()
for (const page of pages) {
const entries = sections.get(page.section) ?? []
entries.push(page)
sections.set(page.section, entries)
}
return [...sections.entries()]
.sort(([left], [right]) => sectionOrder.indexOf(left) - sectionOrder.indexOf(right))
.map(([text, entries]) => ({
text,
items: entries
.sort((left, right) => left.order - right.order)
.map(page => ({ text: page.label, link: `/${page.route.replace(/(?:index)?\.md$/, '')}` })),
}))
}
function watchCanonicalDocs(server: ViteDevServer): void {
const sources = docsSourceFiles()
server.watcher.add(sources)
server.watcher.on('change', (changed) => {
if (!sources.includes(changed)) return
projectDocs()
})
}
function escapeVueInterpolation(html: string): string {
return html.replaceAll('{{', '&#123;&#123;').replaceAll('}}', '&#125;&#125;')
}
const sharedTheme: Pick<DefaultTheme.Config, 'search' | 'socialLinks' | 'editLink'> = {
search: { provider: 'local' },
socialLinks: [
{ icon: 'github', link: 'https://github.com/deepseek-harness/deepseek-harness' },
],
editLink: {
pattern: ({ frontmatter }: PageData) => {
const data: unknown = frontmatter
const editSource: unknown = typeof data === 'object' && data !== null ? Reflect.get(data, 'editSource') : undefined
if (typeof editSource !== 'string') throw new Error('Projected documentation page has no editSource frontmatter.')
return `https://github.com/deepseek-harness/deepseek-harness/edit/master/${editSource}`
},
text: '在 GitHub 上编辑此页',
},
}
export default withMermaid({
title: 'DeepSeek Harness',
description: '用于构建 Agent Harness 的插件化 SDK',
cleanUrls: true,
srcDir: '.generated',
cacheDir: '.cache',
outDir: '.dist',
locales: {
root: {
label: '简体中文',
lang: 'zh-CN',
themeConfig: {
nav: [
{ text: '入门', link: '/guide/', activeMatch: '^/guide/' },
{ text: '开发', link: '/develop/basic/', activeMatch: '^/develop/' },
{ text: 'Reference', link: '/en/', activeMatch: '^/en/' },
],
sidebar: {
'/guide/': sidebar('zh-guide'),
'/develop/': sidebar('zh-develop'),
},
outline: { label: '本页目录' },
docFooter: { prev: '上一篇', next: '下一篇' },
},
},
en: {
label: 'English',
lang: 'en-US',
link: '/en/',
themeConfig: {
nav: [
{ text: 'Concepts', link: '/en/' },
{ text: 'Reference', link: '/en/config-catalog' },
{ text: '中文指南', link: '/guide/' },
],
sidebar: {
'/en/': sidebar('en-docs'),
},
editLink: {
pattern: ({ frontmatter }: PageData) => {
const data: unknown = frontmatter
const editSource: unknown = typeof data === 'object' && data !== null ? Reflect.get(data, 'editSource') : undefined
if (typeof editSource !== 'string') throw new Error('Projected documentation page has no editSource frontmatter.')
return `https://github.com/deepseek-harness/deepseek-harness/edit/master/${editSource}`
},
text: 'Edit this page on GitHub',
},
outline: { label: 'On this page' },
docFooter: { prev: 'Previous', next: 'Next' },
},
},
},
vite: {
plugins: [
{
name: 'deepseek-harness-doc-projector',
configureServer: watchCanonicalDocs,
},
],
},
markdown: {
config(md) {
const renderText = md.renderer.rules.text
const renderCode = md.renderer.rules.code_inline
if (renderText === undefined || renderCode === undefined) {
throw new Error('VitePress Markdown renderer is missing its text or inline-code rule.')
}
md.renderer.rules.text = (...args) => escapeVueInterpolation(renderText(...args))
md.renderer.rules.code_inline = (...args) => escapeVueInterpolation(renderCode(...args))
},
},
mermaid: {},
themeConfig: sharedTheme,
})

View File

@@ -1,17 +0,0 @@
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' },
],
},
})

View File

@@ -1,99 +0,0 @@
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<DefaultTheme.Config> = {
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: '下一篇' },
},
}

213
website/docs.ts Normal file
View File

@@ -0,0 +1,213 @@
/**
* Canonical publication manifest for the documentation website.
*
* Markdown stays in its owning repository tier. This manifest only maps a
* source file to its public route and navigation placement.
*/
/** A page projected into the VitePress source tree. */
export interface DocsPage {
/** Repository-relative canonical Markdown source. */
source: string
/** VitePress route, including the `.md` suffix. */
route: string
/** Navigation label shown in the sidebar. */
label: string
/** Sidebar collection that owns the page. */
sidebar: 'zh-guide' | 'zh-develop' | 'en-docs'
/** Section label within the sidebar. */
section: string
/** Stable order within the section. */
order: number
/** Additional repository paths that resolve to this page. */
sourceAliases?: string[]
}
const zhGuide: DocsPage[] = [
{
source: 'docs/user/zh-CN/index.md',
route: 'index.md',
label: 'DeepSeek Harness',
sidebar: 'zh-guide',
section: '入门',
order: 0,
},
{
source: 'docs/user/zh-CN/guide/index.md',
route: 'guide/index.md',
label: '介绍',
sidebar: 'zh-guide',
section: '入门',
order: 1,
sourceAliases: ['docs/user/zh-CN/guide'],
},
{
source: 'docs/user/zh-CN/guide/quickstart.md',
route: 'guide/quickstart.md',
label: '快速开始',
sidebar: 'zh-guide',
section: '入门',
order: 2,
},
{
source: 'docs/user/zh-CN/guide/config.md',
route: 'guide/config.md',
label: '配置文件',
sidebar: 'zh-guide',
section: '入门',
order: 3,
},
]
const zhDevelop: DocsPage[] = [
{
source: 'docs/user/zh-CN/develop/basic/index.md',
route: 'develop/basic/index.md',
label: '第一个插件',
sidebar: 'zh-develop',
section: '基础',
order: 1,
sourceAliases: ['docs/user/zh-CN/develop/basic'],
},
{
source: 'docs/user/zh-CN/develop/basic/tool.md',
route: 'develop/basic/tool.md',
label: '开发一个 Tool',
sidebar: 'zh-develop',
section: '基础',
order: 2,
},
{
source: 'docs/user/zh-CN/develop/basic/config.md',
route: 'develop/basic/config.md',
label: '插件配置',
sidebar: 'zh-develop',
section: '基础',
order: 3,
},
{
source: 'docs/user/zh-CN/develop/framework/index.md',
route: 'develop/framework/index.md',
label: '插件与生命周期',
sidebar: 'zh-develop',
section: '框架能力',
order: 1,
sourceAliases: ['docs/user/zh-CN/develop/framework'],
},
{
source: 'docs/user/zh-CN/develop/framework/service.md',
route: 'develop/framework/service.md',
label: '服务与依赖',
sidebar: 'zh-develop',
section: '框架能力',
order: 2,
},
{
source: 'docs/user/zh-CN/develop/framework/events.md',
route: 'develop/framework/events.md',
label: '事件系统',
sidebar: 'zh-develop',
section: '框架能力',
order: 3,
},
{
source: 'docs/user/zh-CN/develop/practice/index.md',
route: 'develop/practice/index.md',
label: '能力的三层拆分',
sidebar: 'zh-develop',
section: '实战',
order: 1,
sourceAliases: ['docs/user/zh-CN/develop/practice'],
},
{
source: 'docs/user/zh-CN/develop/practice/llm-adapter.md',
route: 'develop/practice/llm-adapter.md',
label: 'LLM 适配器',
sidebar: 'zh-develop',
section: '实战',
order: 2,
},
]
const enOverview: DocsPage[] = ([
['docs/architecture.md', 'en/index.md', 'Architecture'],
['docs/cordis-primer.md', 'en/cordis-primer.md', 'Cordis primer'],
['docs/capability-seams.md', 'en/capability-seams.md', 'Capability services'],
['docs/agent-lifecycle.md', 'en/agent-lifecycle.md', 'Agent lifecycle'],
['docs/tool-execution-pipeline.md', 'en/tool-execution-pipeline.md', 'Tool execution'],
] as const).map(([source, route, label], order) => ({
source,
route,
label,
sidebar: 'en-docs',
section: 'Concepts',
order,
}))
const enCatalogs: DocsPage[] = ([
['docs/config-catalog.md', 'en/config-catalog.md', 'Plugin configuration'],
['docs/tool-catalog.md', 'en/tool-catalog.md', 'Tool schemas'],
['docs/cordis-catalog/services.md', 'en/cordis-catalog/services.md', 'Services'],
['docs/cordis-catalog/events.md', 'en/cordis-catalog/events.md', 'Events'],
['docs/persistence-catalog.md', 'en/persistence-catalog.md', 'Persistence events'],
] as const).map(([source, route, label], order) => ({
source,
route,
label,
sidebar: 'en-docs',
section: 'Generated reference',
order,
}))
const corePages = [
['core.md', 'Core data structures'],
['session.md', 'Sessions'],
['tools.md', 'Tools'],
['llm-streaming.md', 'LLM streaming'],
['bash.md', 'Bash execution'],
['filesystem.md', 'Filesystem'],
['code-runtime.md', 'Code runtime'],
['compaction.md', 'Compaction'],
['subagent.md', 'Subagents'],
['workflow.md', 'Workflows'],
['skills.md', 'Skills'],
['approval.md', 'Approvals'],
['user-interaction.md', 'User interaction'],
['sandbox.md', 'Sandboxing'],
['web.md', 'Web access'],
['persistence.md', 'Session persistence'],
] as const
const enCore: DocsPage[] = corePages.map(([file, label], order) => ({
source: `docs/core-data-structures/${file}`,
route: `en/core-data-structures/${file}`,
label,
sidebar: 'en-docs',
section: 'Data structures',
order,
...(file === 'core.md' ? { sourceAliases: ['docs/core-data-structures'] } : {}),
}))
const enCookbook: DocsPage[] = ([
['adding-a-package.md', 'Adding a package'],
['adding-a-tool.md', 'Adding a tool'],
['adding-an-llm-adapter.md', 'Adding an LLM adapter'],
['extension-cookbook.md', 'Extension patterns'],
] as const).map(([file, label], order) => ({
source: `docs/cookbook/${file}`,
route: `en/cookbook/${file}`,
label,
sidebar: 'en-docs',
section: 'Cookbook',
order,
}))
/** Every canonical page published by the documentation website. */
export const docsPages: DocsPage[] = [
...zhGuide,
...zhDevelop,
...enOverview,
...enCatalogs,
...enCore,
...enCookbook,
]

View File

@@ -4,12 +4,19 @@
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vitepress dev . --port 5173 --open",
"dev": "vitepress dev . --host 127.0.0.1 --port 5173",
"build": "vitepress build .",
"preview": "vitepress preview ."
"preview": "vitepress preview . --host 127.0.0.1 --port 4173"
},
"devDependencies": {
"vitepress": "^1.6.3",
"vue": "^3.5.13"
"@braintree/sanitize-url": "7.1.2",
"cytoscape": "3.34.0",
"cytoscape-cose-bilkent": "4.1.0",
"dayjs": "1.11.21",
"debug": "4.4.3",
"mermaid": "11.16.0",
"vite": "^5.4.14",
"vitepress": "^1.6.4",
"vitepress-plugin-mermaid": "^2.0.17"
}
}

View File

@@ -1,85 +0,0 @@
# 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 追踪)。

View File

@@ -1,120 +0,0 @@
# 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<void>`
异步触发所有匹配的监听器(并行等待)。
### ctx.bail(thisArg?, event, ...args) {#ctx-bail}
- **返回值:** `any`
同步依次触发监听器。第一个返回非 `undefined`/`null`/`false` 值的监听器停止链并返回该值。
### ctx.serial(thisArg?, event, ...args) {#ctx-serial}
- **返回值:** `Promise<any>`
异步依次触发监听器。语义同 `bail` 的异步版本。
### ctx.waterfall(thisArg?, event, ...args) {#ctx-waterfall}
- **返回值:** `Promise<any>`
管道模式:每个监听器接收前一个的输出。监听器内部必须调用 `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
上下文压缩结束。

View File

@@ -1,108 +0,0 @@
# 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<void>`
手动 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<void>`
使 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 中)
}
```

View File

@@ -1,87 +0,0 @@
# 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 或默认值 |

View File

@@ -1,97 +0,0 @@
# 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
}
}
```

View File

@@ -1,85 +0,0 @@
# 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 IDbranded */
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<StepResult>`
执行一步:将输入发送给模型,获取响应,执行 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')
```

View File

@@ -1,81 +0,0 @@
# 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<BashResult>`
执行一个 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 不变。

View File

@@ -1,78 +0,0 @@
# 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<string>`
读取文件内容。
### ctx.fs.write(path, content)
- **path:** `string`
- **content:** `string`
- **返回值:** `Promise<void>`
写入文件(覆盖)。
### ctx.fs.edit(path, edits)
- **path:** `string`
- **edits:** `Edit[]`
- **返回值:** `Promise<void>`
对文件执行精确的字符串替换编辑。
### ctx.fs.stat(path)
- **path:** `string`
- **返回值:** `Promise<FileStat>`
获取文件/目录信息。
## 配置 (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 层

View File

@@ -1,124 +0,0 @@
# 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<StreamChunk>`
将统一请求格式转换为具体 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 }
```

View File

@@ -1,56 +0,0 @@
# 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实现跨进程恢复。

View File

@@ -1,85 +0,0 @@
# 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<SubagentResult>`
委派一个任务给子代理执行。
## 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** — 需要上下文的子任务(如"基于我们刚才讨论的,去实现这个"),子代理继承父级的对话前缀

View File

@@ -1,122 +0,0 @@
# Tools (dsh-tools)
Tool 注册表和 `defineTool` DSL。
**包名:** `@deepseek-ai/dsh-tools`
**服务名:** `ctx.tools`
## ToolRegistry
### ctx.tools.register(tool)
- **tool:** `ToolDefinition`
- **返回值:** `() => void` disposer
注册一个 tool。返回的 disposer 可手动撤销注册(通常不需要,插件卸载时自动撤销)。
## defineTool\<S\>(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\<S\>
| 字段 | 类型 | 说明 |
|------|------|------|
| `name` | `string` | Tool 名称(全局唯一) |
| `description` | `string` | 发送给模型的描述 |
| `parameters` | `SchemaSpec` | 参数 schema见下文 |
| `execute` | `(args: InferArgs<S>, exec: ToolExecution) => Promise<ToolExecuteReturn>` | 执行函数 |
| `presentCall?` | `(args: InferArgs<S>) => ToolCallView \| undefined` | UI 展示(纯函数) |
| `presentResult?` | `(args: InferArgs<S>, 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<S>` 自动从 `SchemaSpec` 推导 TypeScript 类型:
- `required: true` → 必填字段
-`required` → 可选字段(`?`
- `type: 'object'` + `properties` → 递归推导嵌套对象
- `type: 'array'` + `items` → 推导为数组
## ToolDefinition
运行时 tool 定义(`defineTool` 的返回值):
```typescript
interface ToolDefinition {
name: string
description: string
parameters: Record<string, unknown> // JSON Schema
execute(args: unknown, exec: ToolExecution): Promise<ToolExecuteReturn>
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。

View File

@@ -1,25 +0,0 @@
# API 参考
本节提供 DeepSeek Harness 的完整 API 参考文档,分为两部分:
## 框架 API
Cordis 微内核提供的基础能力,所有插件开发都建立在这些 API 之上:
- [Context](./cordis/context) — 上下文对象,所有服务和方法的入口
- [Events](./cordis/events) — 事件系统 APIemit / 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) — 子代理委派接口

View File

@@ -1,72 +0,0 @@
# 可组合性与插件系统
## 组合
编程的本质就是组合。将小的构建块拼装为更大的系统,再将大系统作为块继续拼装——这是从函数到模块到微服务一脉相承的思想。
组合可以分为两种:
- **静态组合**:编译期确定的组合,例如函数调用、模块导入。
- **动态组合**:运行时确定的组合,例如热更新、插件加载/卸载。
静态组合是逻辑的组合;动态组合为可组合性引入了时间和空间两个新维度。
## 三种可组合性
| 维度 | 定义 | 对应问题 |
|------|------|----------|
| **逻辑可组合性** (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`)消失时,插件自动挂起;恢复时自动重新加载。

View File

@@ -1,129 +0,0 @@
# 上下文模型
上下文 (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 能在保持「一切皆插件」的同时,不给插件开发者增加心智负担——**上下文模型把复杂性封装在了框架内部**。

View File

@@ -1,69 +0,0 @@
# 作用与余作用
## 作用 (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 能被安全热替换的根本原因。

View File

@@ -1,39 +0,0 @@
# 系统设计
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 模式

View File

@@ -1,90 +0,0 @@
# 响应式余作用
响应式余作用 (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` 被调用时,声明的依赖一定已就绪。

View File

@@ -1,128 +0,0 @@
# 可逆作用
可逆作用 (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确保资源完全释放

View File

@@ -1,108 +0,0 @@
# 插件配置
让你的插件接受用户在 `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) — 让你的插件对外提供服务

View File

@@ -1,148 +0,0 @@
# 第一个插件
本文带你编写一个最小的 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) — 让插件接受用户配置

View File

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

View File

@@ -1,152 +0,0 @@
# 事件系统
事件是 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 后端

View File

@@ -1,139 +0,0 @@
# 插件与生命周期
深入了解 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) — 插件间通信的核心机制

View File

@@ -1,147 +0,0 @@
# 服务与依赖
服务 (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 模式中的应用

View File

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

View File

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

View File

@@ -1,342 +0,0 @@
# 配置文件
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 <text>" 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-deepseekDeepSeek 适配器)
**包名:** `@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-localBash 执行器)
**包名:** `@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-webWeb 工具)
**包名:** `@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/) — 查看各插件完整接口

View File

@@ -1,47 +0,0 @@
# 介绍
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

View File

@@ -1,98 +0,0 @@
# 快速开始
本指南带你在 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 <text>" triggers the tool).
>
```
试着输入:
```
> echo hello world
```
你会看到模型发起了一次 tool call工具调用echo 工具将文本转为大写并返回:
```
[tool call] echo({"text":"hello world"})
[tool result] ECHO: HELLO WORLD
```
恭喜!环境没问题。
## 第二步:使用真实模型调用
接下来接入真实的 DeepSeek 模型,跑一个完整的命令行 Agent。
### 获取 API Key
前往 [DeepSeek Platform](https://platform.deepseek.com/) 获取你的 API key。
### 配置环境变量
在仓库根目录创建 `.env` 文件(已被 gitignore
```sh
DEEPSEEK_API_KEY=sk-your-key-here
```
### 启动 coding-agent
```sh
pnpm run demo:repl
```
```
agent REPL ready. Give it a coding task.
>
```
这就是一个完整的编程助手,它能读写文件、跑命令、拆分子任务。
试着给它一个任务:
```
> 在当前目录创建一个 hello.js内容是打印 "Hello from Harness!",然后运行它
```
## 回头看
echo-agent 和 coding-agent 用的是同一个应用框架(`@deepseek-ai/dsh-stdio-agent`),区别只在 `cordis.yml`——换了哪些插件、填了什么配置。你以后定制自己的 Agent 也是同样的方式。
## 下一步
- [配置文件](./config) — 了解 `cordis.yml` 的完整语法
- [开发插件](../develop/basic/) — 编写你自己的 tool 或后端

View File

@@ -1,21 +0,0 @@
---
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 执行、子代理委派等完整工具链,复制模板即可运行。
---