mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
feat(docs): build maintainable documentation site
This commit is contained in:
@@ -15,6 +15,7 @@ Every fact has exactly one home — the tier whose job it is — and every other
|
||||
| [rfc/](rfc/README.md) | Decision records: the why and the what-was-given-up; `implemented/` RFCs describe shipped reality in present tense | Migration plans, test checklists, and spec-speak ("should…") once the decision has shipped |
|
||||
| [postmortem/](postmortem/README.md) | Incident stories — the only tier where war-story narrative belongs | — |
|
||||
| [cookbook/](cookbook/adding-a-package.md) | Step-by-step how-tos with numbered verify steps | Design rationale (→ the RFC each guide links) |
|
||||
| [user/](user/zh-CN/index.md) | Product-facing guides published by the documentation website | Generated reference tables, contributor procedures, decision history |
|
||||
| Package README | The per-package contract: config, semantics, limitations, extension points | JSDoc restatement, generated-catalog restatement (event/tool tables), other packages' concerns |
|
||||
| [development.md](development.md) | First-stop contributor onboarding: local setup, daily workflow, and CI shape at summary level; a bilingual pair under the [i18n contract](i18n/README.md) | Runtime/version rationale (→ RFCs), gate-by-gate enumerations that drift from `package.json` scripts |
|
||||
| Generated catalogs: [cordis events](cordis-catalog/events.md), [cordis services](cordis-catalog/services.md), [tool-catalog](tool-catalog.md), [config-catalog](config-catalog.md), [persistence-catalog](persistence-catalog.md), [module-graph.md](module-graph.md) | Exhaustive enumerations regenerated from source, freshness-gated | Hand edits of any kind |
|
||||
|
||||
@@ -160,6 +160,7 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand;
|
||||
| [Raise the Node LTS engine floor to 22.19](implemented/process/2026-07-06-node-engine-floor.md) | 2026-07-06 |
|
||||
| [Parallel GitHub CI gates](implemented/process/2026-07-06-parallel-github-ci-gates.md) | 2026-07-06 |
|
||||
| [Parallel pre-push gates](implemented/process/2026-07-06-parallel-pre-push-gates.md) | 2026-07-06 |
|
||||
| [Project canonical documentation into the website](implemented/process/2026-07-13-documentation-site-projection.md) | 2026-07-13 |
|
||||
|
||||
### Testing
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# RFC: Project canonical documentation into the website
|
||||
|
||||
Status: implemented
|
||||
|
||||
## Problem
|
||||
|
||||
The repository needs a navigable documentation website without turning the website directory into a second documentation source. Copying package guides, architecture pages, or generated catalogs into a site-specific tree allows the two copies to drift, while pointing VitePress directly at the repository root couples public URLs and navigation to the internal file layout. Repository-relative links also need different destinations on the website: published pages stay inside the site, but source files and unpublished contributor documents belong on GitHub.
|
||||
|
||||
## Decision
|
||||
|
||||
Canonical Markdown remains in the repository tier that owns it. Product-facing guides live under `docs/user/`, generated reference remains in the existing generated catalogs, and architectural and cookbook pages remain at their existing `docs/` paths.
|
||||
|
||||
`website/docs.ts` is an explicit publication manifest. Each entry maps one canonical source file to a stable public route, sidebar, section, and order. Adding or removing a published page is therefore a reviewable manifest change rather than an implicit directory crawl.
|
||||
|
||||
`scripts/project-doc-site.ts` projects the manifest into the ignored `website/.generated/` directory before VitePress starts or builds. The generated tree follows public routes so VitePress navigation, locale detection, and local search share the same route vocabulary. Each page receives an `editSource` frontmatter field pointing to its canonical repository file; the edit-link callback reads only that page data, so public URLs remain independent of the source layout.
|
||||
|
||||
The projector parses Markdown links without reserializing the document. A link to another published source becomes a site-relative route; a link to an unpublished repository file becomes a GitHub source link; a repository image becomes a raw GitHub URL. Missing relative targets fail projection. Unit tests pin these transformations, and `docs:check` runs the projector tests plus a production VitePress build as part of `doc-sync` and the parallel documentation gates.
|
||||
|
||||
Mermaid renders the canonical diagrams. The website workspace explicitly declares the five packages that `vitepress-plugin-mermaid` asks Vite to prebundle because pnpm's strict dependency isolation otherwise makes those transitive packages unavailable to the local development server; Knip records this runtime-only use as an intentional dependency exception.
|
||||
|
||||
Site publication is separate from site construction. The repository contains local development and build commands, but no hosting or deployment workflow until a public destination is chosen.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Commit copied Markdown under `website/`.** This makes VitePress setup direct, but every copied guide or API table gains two owners and requires a synchronization convention that cannot identify which copy is authoritative.
|
||||
|
||||
**Make `website/` the canonical home for every published page.** This keeps one copy but moves architecture, generated reference, and contributor-facing material away from their repository ownership tiers merely to satisfy a renderer.
|
||||
|
||||
**Discover every Markdown file automatically.** This minimizes manifest maintenance but publishes internal documents accidentally, exposes source moves as URL changes, and produces navigation from incidental directory order.
|
||||
|
||||
**Use filesystem symlinks.** Symlinks preserve a single source but do not solve public routing or repository-relative links, and their behavior is less predictable across local development, package tooling, and hosted CI environments.
|
||||
|
||||
**Build only in a deployment workflow.** A deployment job can reveal rendering failures after merge. Keeping the production build in `doc-sync` makes the same failure visible locally and in ordinary CI even when no public deployment exists.
|
||||
|
||||
## Consequences
|
||||
|
||||
Documentation facts have one editable home, public routes remain stable across source moves, and the site can include generated references without committing another generated copy. Local development watches canonical inputs and regenerates the disposable projection.
|
||||
|
||||
The publication manifest is a maintained allowlist, and link projection adds a small repository-specific build adapter. A new kind of Markdown link behavior needs a projector test. Mermaid support also increases the client bundle size, but preserves diagrams already used by the canonical documentation.
|
||||
@@ -105,4 +105,4 @@ export function apply(ctx: Context, config: Config) {
|
||||
## 下一步
|
||||
|
||||
- [插件与生命周期](../framework/) — 深入了解插件的完整生命周期
|
||||
- [服务与依赖](../framework/service) — 让你的插件对外提供服务
|
||||
- [服务与依赖](../framework/service.md) — 让你的插件对外提供服务
|
||||
@@ -115,7 +115,7 @@ export default class MyService extends Service {
|
||||
}
|
||||
```
|
||||
|
||||
大多数情况下,函数形式足够了。类形式用于需要对外提供服务的插件(见 [服务与依赖](../framework/service))。
|
||||
大多数情况下,函数形式足够了。类形式用于需要对外提供服务的插件(见 [服务与依赖](../framework/service.md))。
|
||||
|
||||
## 完整示例
|
||||
|
||||
@@ -144,5 +144,5 @@ export function apply(ctx: Context) {
|
||||
|
||||
## 下一步
|
||||
|
||||
- [开发一个 Tool](./tool) — 详细了解 tool 定义 DSL
|
||||
- [插件配置](./config) — 让插件接受用户配置
|
||||
- [开发一个 Tool](./tool.md) — 详细了解 tool 定义 DSL
|
||||
- [插件配置](./config.md) — 让插件接受用户配置
|
||||
@@ -195,5 +195,5 @@ export function apply(ctx: Context) {
|
||||
|
||||
## 下一步
|
||||
|
||||
- [插件配置](./config) — 让你的 tool 可配置
|
||||
- [插件配置](./config.md) — 让你的 tool 可配置
|
||||
- [能力三件套](../practice/) — 了解 seam/impl/consumer 模式
|
||||
@@ -149,4 +149,4 @@ export function apply(ctx: Context) {
|
||||
## 下一步
|
||||
|
||||
- [能力三件套](../practice/) — 事件在 capability seam 中的角色
|
||||
- [LLM 适配器](../practice/llm-adapter) — 实现一个完整的 LLM 后端
|
||||
- [LLM 适配器](../practice/llm-adapter.md) — 实现一个完整的 LLM 后端
|
||||
@@ -135,5 +135,5 @@ effect cleaned up
|
||||
|
||||
## 下一步
|
||||
|
||||
- [服务与依赖](./service) — 让你的插件对外提供能力
|
||||
- [事件系统](./events) — 插件间通信的核心机制
|
||||
- [服务与依赖](./service.md) — 让你的插件对外提供能力
|
||||
- [事件系统](./events.md) — 插件间通信的核心机制
|
||||
@@ -127,21 +127,11 @@ export const inject = { optional: ['metrics'] }
|
||||
|
||||
`plugin-a` 和 `plugin-b` 各自看到自己组内的 bash 实例,互不影响。
|
||||
|
||||
## Harness 内置服务一览
|
||||
## 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 | 会话持久化 |
|
||||
服务名、公开方法和源码位置由仓库自动生成,见[服务目录](../../../../cordis-catalog/services.md)。开发插件时应以该目录和服务接口的 TypeScript 类型为准,不要复制一份静态清单。
|
||||
|
||||
## 下一步
|
||||
|
||||
- [事件系统](./events) — 插件间松耦合通信
|
||||
- [事件系统](./events.md) — 插件间松耦合通信
|
||||
- [能力三件套](../practice/) — 服务在 seam 模式中的应用
|
||||
@@ -58,7 +58,7 @@
|
||||
| 文件系统 | `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) |
|
||||
| 压缩 | `dsh-compact` | `dsh-compact-basic` | 由实现插件消费 agent-loop 的扩展事件 |
|
||||
|
||||
## 开发你自己的三件套
|
||||
|
||||
@@ -153,4 +153,4 @@ export function apply(ctx: Context) {
|
||||
|
||||
## 下一步
|
||||
|
||||
- [LLM 适配器](./llm-adapter) — 实现一个 LLM 后端(最常见的 seam 扩展)
|
||||
- [LLM 适配器](./llm-adapter.md) — 实现一个 LLM 后端(最常见的 seam 扩展)
|
||||
57
docs/user/zh-CN/guide/config.md
Normal file
57
docs/user/zh-CN/guide/config.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# 配置文件
|
||||
|
||||
Harness 使用 `cordis.yml` 描述 Agent 加载哪些插件以及每个插件的参数。配置文件负责组合能力;每个包真正支持的字段和默认值由源码生成的配置目录负责记录,避免两份手写表格逐渐不一致。
|
||||
|
||||
## 从真实配置开始
|
||||
|
||||
仓库中的示例就是可以运行的配置,也是新项目最可靠的起点:
|
||||
|
||||
- [echo-agent](../../../../examples/echo-agent/cordis.yml) 使用本地 mock 模型,不需要 API key。
|
||||
- [coding-agent](../../../../examples/coding-agent/cordis.yml) 组合 DeepSeek 模型、Bash、文件系统、压缩、子代理和工作流。
|
||||
- [acp-agent](../../../../examples/acp-agent/cordis.yml) 通过 ACP 接入编辑器客户端。
|
||||
|
||||
最小配置由一组插件条目组成:
|
||||
|
||||
```yaml
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
models:
|
||||
- deepseek-v4-flash
|
||||
|
||||
- id: stdio-agent
|
||||
name: '@deepseek-ai/dsh-stdio-agent'
|
||||
config:
|
||||
model: deepseek-v4-flash
|
||||
```
|
||||
|
||||
## 插件条目
|
||||
|
||||
`name` 指定 npm 包或相对于 `cordis.yml` 的本地模块,`id` 为插件实例提供稳定标识,`config` 传入插件自己的配置。需要临时跳过某个条目时可设置 `disabled: true`。
|
||||
|
||||
```yaml
|
||||
- id: local-tool
|
||||
name: './src/my-tool.ts'
|
||||
disabled: false
|
||||
config:
|
||||
toolName: my_tool
|
||||
```
|
||||
|
||||
插件按文件中的顺序加载。依赖其他服务的插件应该排在提供这些服务的应用或能力插件之后;引用不存在的模型、工具或插件会尽早报错,而不是被静默忽略。
|
||||
|
||||
## JavaScript 值和环境变量
|
||||
|
||||
Cordis loader 使用 `!!js` 标签读取运行时表达式。API key 等凭据应放在仓库根目录、已被 Git 忽略的 `.env` 中,不能提交到配置文件。
|
||||
|
||||
```yaml
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
cwd: !!js process.cwd()
|
||||
```
|
||||
|
||||
标签是 `!!js`,不是 `!js`。
|
||||
|
||||
## 精确配置参考
|
||||
|
||||
每个插件当前支持的字段、类型和默认值见自动生成的[插件配置目录](../../../config-catalog.md)。理解插件如何组合可继续阅读[架构说明](../../../architecture.md)和[能力接口](../../../capability-seams.md);要创建自己的配置,优先复制并修改[示例目录说明](../../../../examples/README.md)中最接近的例子。
|
||||
@@ -28,7 +28,7 @@ Harness 将一个 AI Agent(智能体) 所需要的所有能力——LLM 调
|
||||
2. 填写 API key
|
||||
3. 运行
|
||||
|
||||
不需要写任何代码。详见 [快速开始](./quickstart)。
|
||||
不需要写任何代码。详见 [快速开始](./quickstart.md)。
|
||||
|
||||
### 插件开发者
|
||||
|
||||
@@ -41,7 +41,7 @@ Harness 将一个 AI Agent(智能体) 所需要的所有能力——LLM 调
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **运行时**: Node.js >= 24
|
||||
- **运行时**: Node.js ^22.19 或 >= 24
|
||||
- **语言**: TypeScript (ESM)
|
||||
- **框架**: Cordis
|
||||
- **包管理**: pnpm workspaces
|
||||
- **包管理**: pnpm workspaces(仓库固定使用 pnpm 11)
|
||||
@@ -4,13 +4,14 @@
|
||||
|
||||
## 环境准备
|
||||
|
||||
- [Node.js](https://nodejs.org/) >= 24
|
||||
- [pnpm](https://pnpm.io/) >= 9
|
||||
- [Node.js](https://nodejs.org/) ^22.19 或 >= 24
|
||||
- [pnpm](https://pnpm.io/) 11(建议通过 Corepack 使用仓库固定的版本)
|
||||
|
||||
```sh
|
||||
# 确认版本
|
||||
node -v # v24.x 或更高
|
||||
pnpm -v # 9.x 或更高
|
||||
node -v # v22.19.x,或 v24.x 及更高版本
|
||||
corepack enable
|
||||
pnpm -v # 11.x
|
||||
```
|
||||
|
||||
## 第一步:运行 echo-agent
|
||||
@@ -24,8 +25,6 @@ cd deepseek-harness
|
||||
|
||||
# 安装依赖
|
||||
pnpm install
|
||||
# 如果看到 ERR_PNPM_IGNORED_BUILDS,可以忽略——安装已经成功了。
|
||||
# 想消除这个提示可以跑一次: pnpm approve-builds
|
||||
|
||||
# 启动 echo-agent
|
||||
pnpm run demo:echo
|
||||
@@ -94,5 +93,5 @@ echo-agent 和 coding-agent 用的是同一个应用框架(`@deepseek-ai/dsh-std
|
||||
|
||||
## 下一步
|
||||
|
||||
- [配置文件](./config) — 了解 `cordis.yml` 的完整语法
|
||||
- [配置文件](./config.md) — 了解 `cordis.yml` 的完整语法
|
||||
- [开发插件](../develop/basic/) — 编写你自己的 tool 或后端
|
||||
@@ -7,10 +7,10 @@ hero:
|
||||
actions:
|
||||
- theme: brand
|
||||
text: 快速开始
|
||||
link: /zh-CN/guide/quickstart
|
||||
link: /guide/quickstart
|
||||
- theme: alt
|
||||
text: 开发插件
|
||||
link: /zh-CN/develop/basic/
|
||||
link: /develop/basic/
|
||||
features:
|
||||
- title: 插件化架构
|
||||
details: 基于 Cordis 效果系统,所有能力通过插件注册,加载即生效、卸载即还原。
|
||||
@@ -24,6 +24,7 @@ export default tseslint.config(
|
||||
'**/.sessions/**',
|
||||
'.claude/**', // harness-local state (worktrees, skills) — other checkouts, not this one's sources
|
||||
'**/.doc-typecheck-*/**',
|
||||
'website/.generated/**',
|
||||
'vendor/**', // vendored source keeps upstream style and idioms
|
||||
'**/*.js',
|
||||
'**/*.mjs',
|
||||
@@ -33,7 +34,7 @@ export default tseslint.config(
|
||||
|
||||
// --- our packages: full strictness -------------------------------------
|
||||
{
|
||||
files: ['packages/*/*/src/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts'],
|
||||
files: ['packages/*/*/src/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts', 'website/**/*.ts'],
|
||||
extends: [
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
],
|
||||
@@ -125,7 +126,7 @@ export default tseslint.config(
|
||||
|
||||
// --- formatting (everything we own) -------------------------------------
|
||||
{
|
||||
files: ['packages/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts', 'eslint.config.mjs'],
|
||||
files: ['packages/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts', 'website/**/*.ts', 'eslint.config.mjs'],
|
||||
plugins: { '@stylistic': stylistic },
|
||||
rules: {
|
||||
'@stylistic/indent': ['error', 2],
|
||||
|
||||
10
knip.json
10
knip.json
@@ -15,6 +15,16 @@
|
||||
],
|
||||
"project": ["scripts/**/*.ts", "examples/**/*.ts"]
|
||||
},
|
||||
"website": {
|
||||
"project": ["**/*.ts"],
|
||||
"ignoreDependencies": [
|
||||
"@braintree/sanitize-url",
|
||||
"cytoscape",
|
||||
"cytoscape-cose-bilkent",
|
||||
"dayjs",
|
||||
"debug"
|
||||
]
|
||||
},
|
||||
"packages/*/*": {
|
||||
"entry": ["tests/**/*.spec.ts"],
|
||||
"project": ["src/**/*.ts", "tests/**/*.ts"]
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
},
|
||||
"workspaces": [
|
||||
"vendor/*",
|
||||
"packages/*/*"
|
||||
"packages/*/*",
|
||||
"website"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -b tsconfig.build.json && tsdown",
|
||||
@@ -44,6 +45,10 @@
|
||||
"verify-type-equiv": "tsx scripts/verify-type-equiv.ts",
|
||||
"verify-translation-pairing": "tsx scripts/verify-translation-pairing.ts",
|
||||
"verify-doc-budgets": "tsx scripts/verify-doc-budgets.ts",
|
||||
"docs:dev": "pnpm --filter @deepseek-ai/website run dev",
|
||||
"docs:build": "pnpm --filter @deepseek-ai/website run build",
|
||||
"docs:preview": "pnpm --filter @deepseek-ai/website run preview",
|
||||
"docs:check": "pnpm exec vitest run scripts/project-doc-site.spec.ts && pnpm run docs:build",
|
||||
"verify-node-next-types": "tsx scripts/verify-node-next-types.ts",
|
||||
"gen-cordis-catalog": "tsx scripts/gen-cordis-catalog.ts",
|
||||
"gen-rfc-index": "tsx scripts/gen-rfc-index.ts",
|
||||
@@ -62,7 +67,7 @@
|
||||
"gen-module-graph": "tsx scripts/gen-module-graph.ts",
|
||||
"verify-module-graph": "tsx scripts/gen-module-graph.ts --check",
|
||||
"constraints": "tsx scripts/check-workspace-constraints.ts",
|
||||
"doc-sync": "pnpm run doc-typecheck && pnpm run verify-cordis-catalog && pnpm run verify-cordis-api && pnpm run verify-export-jsdoc && pnpm run verify-tool-catalog && pnpm run verify-config-catalog && pnpm run verify-persistence-catalog && pnpm run verify-doc-graphs && pnpm run verify-md-wrap && pnpm run verify-md-links && pnpm run verify-doc-refs && pnpm run verify-package-paths && pnpm run verify-mermaid && pnpm run verify-rfc-classification && pnpm run verify-rfc-format && pnpm run verify-type-equiv && pnpm run verify-translation-pairing && pnpm run verify-doc-budgets",
|
||||
"doc-sync": "pnpm run doc-typecheck && pnpm run verify-cordis-catalog && pnpm run verify-cordis-api && pnpm run verify-export-jsdoc && pnpm run verify-tool-catalog && pnpm run verify-config-catalog && pnpm run verify-persistence-catalog && pnpm run verify-doc-graphs && pnpm run verify-md-wrap && pnpm run verify-md-links && pnpm run verify-doc-refs && pnpm run verify-package-paths && pnpm run verify-mermaid && pnpm run verify-rfc-classification && pnpm run verify-rfc-format && pnpm run verify-type-equiv && pnpm run verify-translation-pairing && pnpm run verify-doc-budgets && pnpm run docs:check",
|
||||
"hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-node-next-types",
|
||||
"demo:echo": "node --expose-internals --import tsx packages/ui/stdio-agent/src/bin.ts examples/echo-agent/cordis.yml",
|
||||
"demo:repl": "node --expose-internals --import tsx packages/ui/stdio-agent/src/bin.ts examples/coding-agent/cordis.yml",
|
||||
|
||||
1565
pnpm-lock.yaml
generated
1565
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
packages:
|
||||
- vendor/*
|
||||
- packages/*/*
|
||||
- website
|
||||
|
||||
peerDependencyRules:
|
||||
allowedVersions:
|
||||
|
||||
98
scripts/project-doc-site.spec.ts
Normal file
98
scripts/project-doc-site.spec.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/** Tests for the documentation website projection adapter. */
|
||||
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import type { DocsPage } from '../website/docs.ts'
|
||||
import { addProjectionFrontmatter, rewriteMarkdown } from './project-doc-site.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
function fixture(): { root: string; pages: DocsPage[] } {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-doc-site-'))
|
||||
roots.push(root)
|
||||
mkdirSync(join(root, 'docs'), { recursive: true })
|
||||
mkdirSync(join(root, 'packages'), { recursive: true })
|
||||
writeFileSync(join(root, 'docs/a.md'), '# A\n')
|
||||
writeFileSync(join(root, 'docs/b.md'), '# B\n')
|
||||
writeFileSync(join(root, 'packages/tool.ts'), 'one\ntwo\n')
|
||||
writeFileSync(join(root, 'packages/logo.svg'), '<svg/>\n')
|
||||
return {
|
||||
root,
|
||||
pages: [
|
||||
{ source: 'docs/a.md', route: 'en/a.md', label: 'A', sidebar: 'en-docs', section: 'Test', order: 1 },
|
||||
{ source: 'docs/b.md', route: 'en/reference/b.md', label: 'B', sidebar: 'en-docs', section: 'Test', order: 2 },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
describe('rewriteMarkdown', () => {
|
||||
it('maps published pages and pins unpublished source links', () => {
|
||||
const { root, pages } = fixture()
|
||||
const source = '[B](b.md#part) [source](../packages/tool.ts:2) [web](https://example.com)\n'
|
||||
expect(rewriteMarkdown(source, {
|
||||
sourcePath: 'docs/a.md',
|
||||
route: 'en/a.md',
|
||||
pages,
|
||||
repoRoot: root,
|
||||
repositoryRef: 'abc123',
|
||||
})).toBe(
|
||||
'[B](./reference/b.md#part) '
|
||||
+ '[source](https://github.com/deepseek-harness/deepseek-harness/blob/abc123/packages/tool.ts#L2) '
|
||||
+ '[web](https://example.com)\n',
|
||||
)
|
||||
})
|
||||
|
||||
it('uses raw GitHub content for unpublished images', () => {
|
||||
const { root, pages } = fixture()
|
||||
expect(rewriteMarkdown('\n', {
|
||||
sourcePath: 'docs/a.md',
|
||||
route: 'en/a.md',
|
||||
pages,
|
||||
repoRoot: root,
|
||||
repositoryRef: 'abc123',
|
||||
})).toBe('\n')
|
||||
})
|
||||
|
||||
it('does not rewrite Markdown-looking text inside code fences', () => {
|
||||
const { root, pages } = fixture()
|
||||
const source = '```md\n[B](b.md)\n```\n'
|
||||
expect(rewriteMarkdown(source, {
|
||||
sourcePath: 'docs/a.md',
|
||||
route: 'en/a.md',
|
||||
pages,
|
||||
repoRoot: root,
|
||||
repositoryRef: 'abc123',
|
||||
})).toBe(source)
|
||||
})
|
||||
|
||||
it('fails loud when a relative target is missing', () => {
|
||||
const { root, pages } = fixture()
|
||||
expect(() => rewriteMarkdown('[missing](missing.md)\n', {
|
||||
sourcePath: 'docs/a.md',
|
||||
route: 'en/a.md',
|
||||
pages,
|
||||
repoRoot: root,
|
||||
repositoryRef: 'abc123',
|
||||
})).toThrow('links to missing path "missing.md"')
|
||||
})
|
||||
})
|
||||
|
||||
describe('addProjectionFrontmatter', () => {
|
||||
it('adds frontmatter to an ordinary Markdown page', () => {
|
||||
expect(addProjectionFrontmatter('# Guide\n', 'docs/guide.md')).toBe(
|
||||
'---\neditSource: "docs/guide.md"\n---\n\n# Guide\n',
|
||||
)
|
||||
})
|
||||
|
||||
it('extends existing VitePress frontmatter', () => {
|
||||
expect(addProjectionFrontmatter('---\nlayout: home\n---\n', 'docs/index.md')).toBe(
|
||||
'---\neditSource: "docs/index.md"\nlayout: home\n---\n',
|
||||
)
|
||||
})
|
||||
})
|
||||
215
scripts/project-doc-site.ts
Normal file
215
scripts/project-doc-site.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* Build-time projection from canonical repository Markdown into VitePress.
|
||||
*
|
||||
* The generated tree is disposable: sources stay in their owning `docs/`
|
||||
* tier, while this adapter rewrites cross-source links for the public site.
|
||||
*/
|
||||
|
||||
import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, extname, posix, relative, resolve, sep } from 'node:path'
|
||||
import { fromMarkdown } from 'mdast-util-from-markdown'
|
||||
import { gfmFromMarkdown } from 'mdast-util-gfm'
|
||||
import { gfm } from 'micromark-extension-gfm'
|
||||
import type { Nodes } from 'mdast'
|
||||
import { docsPages, type DocsPage } from '../website/docs.ts'
|
||||
|
||||
const REPOSITORY_URL = 'https://github.com/deepseek-harness/deepseek-harness'
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const generatedRoot = resolve(root, 'website/.generated')
|
||||
|
||||
interface Replacement {
|
||||
start: number
|
||||
end: number
|
||||
value: string
|
||||
}
|
||||
|
||||
/** Inputs for rewriting one canonical Markdown page. */
|
||||
export interface RewriteMarkdownOptions {
|
||||
sourcePath: string
|
||||
route: string
|
||||
pages: DocsPage[]
|
||||
repoRoot: string
|
||||
repositoryRef: string
|
||||
}
|
||||
|
||||
function repoPath(absPath: string, repoRoot: string): string {
|
||||
return relative(repoRoot, absPath).split(sep).join('/')
|
||||
}
|
||||
|
||||
function isExternalOrSiteAbsolute(url: string): boolean {
|
||||
return url.startsWith('#')
|
||||
|| url.startsWith('//')
|
||||
|| url.startsWith('/')
|
||||
|| /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)
|
||||
}
|
||||
|
||||
function splitTarget(url: string): { path: string; suffix: string } {
|
||||
const boundary = url.search(/[?#]/)
|
||||
if (boundary === -1) return { path: url, suffix: '' }
|
||||
return { path: url.slice(0, boundary), suffix: url.slice(boundary) }
|
||||
}
|
||||
|
||||
function decodePath(path: string): string {
|
||||
try {
|
||||
return decodeURIComponent(path)
|
||||
} catch {
|
||||
throw new Error(`project-doc-site: malformed percent escape in ${JSON.stringify(path)}.`)
|
||||
}
|
||||
}
|
||||
|
||||
function routeTarget(fromRoute: string, toRoute: string, suffix: string): string {
|
||||
const target = posix.relative(posix.dirname(fromRoute), toRoute)
|
||||
return `${target.startsWith('.') ? target : `./${target}`}${suffix}`
|
||||
}
|
||||
|
||||
function sourceMap(pages: DocsPage[]): Map<string, DocsPage> {
|
||||
const map = new Map<string, DocsPage>()
|
||||
for (const page of pages) {
|
||||
for (const source of [page.source, ...(page.sourceAliases ?? [])]) {
|
||||
if (map.has(source)) {
|
||||
throw new Error(`project-doc-site: duplicate source or alias ${JSON.stringify(source)}.`)
|
||||
}
|
||||
map.set(source, page)
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
function resolveRepositoryTarget(sourceAbs: string, rawPath: string, repoRoot: string): { absPath: string; line?: number } {
|
||||
const decoded = decodePath(rawPath)
|
||||
let absPath = resolve(dirname(sourceAbs), decoded)
|
||||
if (existsSync(absPath)) return { absPath }
|
||||
|
||||
const lineMatch = decoded.match(/:(\d+)$/)
|
||||
if (lineMatch !== null) {
|
||||
const lineText = lineMatch[1]
|
||||
if (lineText === undefined) throw new Error('project-doc-site: line suffix matched without a line number.')
|
||||
absPath = resolve(dirname(sourceAbs), decoded.slice(0, -lineMatch[0].length))
|
||||
if (existsSync(absPath)) return { absPath, line: Number.parseInt(lineText, 10) }
|
||||
}
|
||||
|
||||
if (extname(decoded) === '') {
|
||||
const markdown = resolve(dirname(sourceAbs), `${decoded}.md`)
|
||||
if (existsSync(markdown)) return { absPath: markdown }
|
||||
const index = resolve(dirname(sourceAbs), decoded, 'index.md')
|
||||
if (existsSync(index)) return { absPath: index }
|
||||
}
|
||||
|
||||
throw new Error(`project-doc-site: ${repoPath(sourceAbs, repoRoot)} links to missing path ${JSON.stringify(rawPath)}.`)
|
||||
}
|
||||
|
||||
function githubTarget(
|
||||
absPath: string,
|
||||
line: number | undefined,
|
||||
suffix: string,
|
||||
repositoryRef: string,
|
||||
repoRoot: string,
|
||||
image: boolean,
|
||||
): string {
|
||||
const path = repoPath(absPath, repoRoot)
|
||||
if (image) return `https://raw.githubusercontent.com/deepseek-harness/deepseek-harness/${repositoryRef}/${path}${suffix}`
|
||||
const kind = lstatSync(absPath).isDirectory() ? 'tree' : 'blob'
|
||||
const lineSuffix = line === undefined ? suffix : `#L${line}`
|
||||
return `${REPOSITORY_URL}/${kind}/${repositoryRef}/${path}${lineSuffix}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrite repository-relative links without reserializing Markdown.
|
||||
*
|
||||
* @param source Markdown text from the canonical file.
|
||||
* @param options Source, route, manifest, and repository context.
|
||||
* @returns Markdown whose published links resolve inside the site or to GitHub.
|
||||
*/
|
||||
export function rewriteMarkdown(source: string, options: RewriteMarkdownOptions): string {
|
||||
const sourceAbs = resolve(options.repoRoot, options.sourcePath)
|
||||
const published = sourceMap(options.pages)
|
||||
const tree = fromMarkdown(source, { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] })
|
||||
const replacements: Replacement[] = []
|
||||
|
||||
const rewrite = (node: Nodes & { url: string }): void => {
|
||||
if (isExternalOrSiteAbsolute(node.url)) return
|
||||
const { path, suffix } = splitTarget(node.url)
|
||||
if (path === '') return
|
||||
const { absPath, line } = resolveRepositoryTarget(sourceAbs, path, options.repoRoot)
|
||||
const targetPath = repoPath(absPath, options.repoRoot)
|
||||
const page = published.get(targetPath)
|
||||
const nextUrl = page === undefined
|
||||
? githubTarget(absPath, line, suffix, options.repositoryRef, options.repoRoot, node.type === 'image')
|
||||
: routeTarget(options.route, page.route, suffix)
|
||||
|
||||
const start = node.position?.start.offset
|
||||
const end = node.position?.end.offset
|
||||
if (start === undefined || end === undefined) {
|
||||
throw new Error(`project-doc-site: link ${JSON.stringify(node.url)} has no source offsets.`)
|
||||
}
|
||||
const rawNode = source.slice(start, end)
|
||||
const urlOffset = rawNode.lastIndexOf(node.url)
|
||||
if (urlOffset === -1) {
|
||||
throw new Error(`project-doc-site: cannot locate raw target ${JSON.stringify(node.url)} in ${JSON.stringify(rawNode)}.`)
|
||||
}
|
||||
replacements.push({
|
||||
start: start + urlOffset,
|
||||
end: start + urlOffset + node.url.length,
|
||||
value: nextUrl,
|
||||
})
|
||||
}
|
||||
|
||||
const visit = (node: Nodes): void => {
|
||||
if ((node.type === 'link' || node.type === 'image' || node.type === 'definition') && 'url' in node) rewrite(node)
|
||||
if ('children' in node) {
|
||||
for (const child of node.children) visit(child)
|
||||
}
|
||||
}
|
||||
visit(tree)
|
||||
|
||||
let projected = source
|
||||
for (const replacement of replacements.sort((a, b) => b.start - a.start)) {
|
||||
projected = projected.slice(0, replacement.start) + replacement.value + projected.slice(replacement.end)
|
||||
}
|
||||
return projected
|
||||
}
|
||||
|
||||
/**
|
||||
* Record the canonical edit target in VitePress frontmatter.
|
||||
*
|
||||
* @param markdown Projected Markdown content.
|
||||
* @param sourcePath Repository-relative canonical source path.
|
||||
* @returns Markdown with an `editSource` frontmatter field.
|
||||
*/
|
||||
export function addProjectionFrontmatter(markdown: string, sourcePath: string): string {
|
||||
const field = `editSource: ${JSON.stringify(sourcePath)}`
|
||||
if (markdown.startsWith('---\n')) return markdown.replace('---\n', `---\n${field}\n`)
|
||||
return `---\n${field}\n---\n\n${markdown}`
|
||||
}
|
||||
|
||||
/** Canonical Markdown files watched by the local VitePress dev server. */
|
||||
export function docsSourceFiles(): string[] {
|
||||
return [...new Set(docsPages.map(page => resolve(root, page.source)))]
|
||||
}
|
||||
|
||||
/** Rebuild the disposable VitePress source tree from the publication manifest. */
|
||||
export function projectDocs(): void {
|
||||
const routes = new Set<string>()
|
||||
const repositoryRef = process.env.GITHUB_SHA ?? 'master'
|
||||
rmSync(generatedRoot, { recursive: true, force: true })
|
||||
|
||||
for (const page of docsPages) {
|
||||
if (routes.has(page.route)) throw new Error(`project-doc-site: duplicate route ${JSON.stringify(page.route)}.`)
|
||||
routes.add(page.route)
|
||||
const sourceAbs = resolve(root, page.source)
|
||||
if (!existsSync(sourceAbs) || !lstatSync(sourceAbs).isFile()) {
|
||||
throw new Error(`project-doc-site: source ${JSON.stringify(page.source)} does not exist or is not a file.`)
|
||||
}
|
||||
const output = resolve(generatedRoot, page.route)
|
||||
mkdirSync(dirname(output), { recursive: true })
|
||||
const markdown = readFileSync(sourceAbs, 'utf8')
|
||||
const projected = rewriteMarkdown(markdown, {
|
||||
sourcePath: page.source,
|
||||
route: page.route,
|
||||
pages: docsPages,
|
||||
repoRoot: root,
|
||||
repositoryRef,
|
||||
})
|
||||
writeFileSync(output, addProjectionFrontmatter(projected, page.source))
|
||||
}
|
||||
}
|
||||
@@ -272,6 +272,7 @@ function docSyncLeafGates(): Gate[] {
|
||||
pnpmScript('type-equivalence', 'verify-type-equiv', { label: 'type equivalence' }),
|
||||
pnpmScript('translation-pairing', 'verify-translation-pairing', { label: 'translation pairing' }),
|
||||
pnpmScript('doc-budgets', 'verify-doc-budgets', { label: 'doc budgets' }),
|
||||
pnpmScript('docs-site', 'docs:check', { label: 'documentation site' }),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,12 @@
|
||||
* `paragraph` node whose source span covers more than one line. The parser owns
|
||||
* all the structure that legitimately occupies multiple lines — fenced code
|
||||
* (any fence length), tables, list items, blockquotes, HTML blocks, headings,
|
||||
* thematic breaks, link-reference definitions — so a hard wrap is simply "a
|
||||
* paragraph node that starts and ends on different lines." This is checker, not
|
||||
* formatter: it reports and never rewrites, so it introduces zero cosmetic
|
||||
* churn (no emphasis-marker or table-delimiter normalization).
|
||||
* thematic breaks, link-reference definitions — while a small preprocessing
|
||||
* pass masks VitePress YAML frontmatter and custom-container delimiter lines.
|
||||
* A hard wrap is simply "a paragraph node that starts and ends on different
|
||||
* lines." This is checker, not formatter: it reports and never rewrites, so it
|
||||
* introduces zero cosmetic churn (no emphasis-marker or table-delimiter
|
||||
* normalization).
|
||||
*
|
||||
* A wrapped paragraph inside a list item or blockquote is still a `paragraph`
|
||||
* node, so those are caught too. Scope mirrors doc-typecheck plus the two
|
||||
@@ -57,11 +59,23 @@ interface Violation {
|
||||
text: string
|
||||
}
|
||||
|
||||
function maskVitePressStructure(source: string): string {
|
||||
const lines = source.split('\n')
|
||||
if (lines[0] === '---') {
|
||||
const closing = lines.indexOf('---', 1)
|
||||
if (closing !== -1) {
|
||||
for (let index = 0; index <= closing; index++) lines[index] = ''
|
||||
}
|
||||
}
|
||||
return lines.map(line => line.trimStart().startsWith(':::') ? '' : line).join('\n')
|
||||
}
|
||||
|
||||
/** Find every hard-wrapped prose paragraph in one Markdown file via its AST. */
|
||||
function findViolations(absPath: string): Violation[] {
|
||||
const file = relative(root, absPath)
|
||||
const source = readFileSync(absPath, 'utf8')
|
||||
const tree = fromMarkdown(source, { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] })
|
||||
const parsedSource = maskVitePressStructure(source)
|
||||
const tree = fromMarkdown(parsedSource, { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] })
|
||||
const out: Violation[] = []
|
||||
|
||||
const visit = (node: Nodes): void => {
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
"examples/*/start.ts",
|
||||
"examples/*/tests/**/*.ts",
|
||||
"packages/*/*/tests/**/*.ts",
|
||||
"scripts/**/*.ts"
|
||||
"scripts/**/*.ts",
|
||||
"website/**/*.ts",
|
||||
"website/.vitepress/**/*.ts"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "./vendor/cosmokit" },
|
||||
|
||||
@@ -19,7 +19,7 @@ export default defineConfig({
|
||||
// instead applies the one root map to every importer.
|
||||
plugins: [tsconfigPaths({ projects: ['./tsconfig.json'] })],
|
||||
test: {
|
||||
include: ['packages/*/*/tests/**/*.spec.ts', 'examples/*/tests/**/*.spec.ts'],
|
||||
include: ['packages/*/*/tests/**/*.spec.ts', 'examples/*/tests/**/*.spec.ts', 'scripts/**/*.spec.ts'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
// Coverage measures OUR runtime source. Types-only files carry no
|
||||
|
||||
5
website/.gitignore
vendored
5
website/.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
node_modules/
|
||||
.vitepress/dist/
|
||||
.vitepress/cache/
|
||||
.cache/
|
||||
.dist/
|
||||
.generated/
|
||||
|
||||
142
website/.vitepress/config.ts
Normal file
142
website/.vitepress/config.ts
Normal 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('{{', '{{').replaceAll('}}', '}}')
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
@@ -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' },
|
||||
],
|
||||
},
|
||||
})
|
||||
@@ -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
213
website/docs.ts
Normal 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,
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 追踪)。
|
||||
@@ -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
|
||||
|
||||
上下文压缩结束。
|
||||
@@ -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 中)
|
||||
}
|
||||
```
|
||||
@@ -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 或默认值 |
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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 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<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')
|
||||
```
|
||||
@@ -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 不变。
|
||||
@@ -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 层
|
||||
@@ -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 }
|
||||
```
|
||||
@@ -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),实现跨进程恢复。
|
||||
@@ -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** — 需要上下文的子任务(如"基于我们刚才讨论的,去实现这个"),子代理继承父级的对话前缀
|
||||
@@ -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。
|
||||
@@ -1,25 +0,0 @@
|
||||
# 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) — 子代理委派接口
|
||||
@@ -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`)消失时,插件自动挂起;恢复时自动重新加载。
|
||||
@@ -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 能在保持「一切皆插件」的同时,不给插件开发者增加心智负担——**上下文模型把复杂性封装在了框架内部**。
|
||||
@@ -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 能被安全热替换的根本原因。
|
||||
@@ -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 模式
|
||||
@@ -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` 被调用时,声明的依赖一定已就绪。
|
||||
@@ -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,确保资源完全释放
|
||||
@@ -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-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/) — 查看各插件完整接口
|
||||
Reference in New Issue
Block a user