mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
website: wire the site into the repo gates; make every tutorial example compile
- website joins the pnpm workspace; root scripts website:dev/website:build; run-gates gains a website-build gate (ci-primary + ci-static) — the VitePress build doubles as the site's dead-link check; AGENTS.md documents the commands. - doc-typecheck + verify-type-equiv now scan website/zh-CN/**/*.md; every ```typescript fence converted to ```ts and made standalone-compilable (55 compiled, 1 ignore-check). Phantom APIs the compiler caught are fixed: invented event names (agent/turn-end, tool/call, llm/pre-request, ready, dispose) replaced with real catalog events or per-plugin declare-module merges; presentCall/inject/Config claims corrected to the real shapes. - guide/config.md entry-fields table completed against loader EntryOptions; its coding-agent example brought in line with examples/coding-agent.
This commit is contained in:
@@ -30,6 +30,7 @@ packages/ Harness packages at packages/<group>/<pkg>/, all named @deepseek-ai
|
||||
examples/ Runnable demos: thin cordis.yml leaves over the app packages (see examples/AGENTS.md)
|
||||
docs/ architecture, generated catalogs, RFCs, postmortems, cookbook (see docs/AGENTS.md)
|
||||
scripts/ repo gates and generators
|
||||
website/ VitePress docs site (zh-CN)
|
||||
```
|
||||
|
||||
Per-package map: the group READMEs, indexed from [packages/README.md](packages/README.md).
|
||||
@@ -48,6 +49,7 @@ pnpm run lint
|
||||
pnpm run build # tsc emits lib/types, tsdown bundles runtime
|
||||
pnpm run hygiene # knip + publint + workspace constraints + NodeNext consumer check
|
||||
pnpm run doc-sync # all documentation gates; see the doc-sync script in package.json
|
||||
pnpm run website:build # VitePress build (doubles as the site's dead-link check)
|
||||
pnpm run demo:echo # mock-model REPL, no key needed
|
||||
pnpm run demo:repl # real REPL coding agent (needs DEEPSEEK_API_KEY)
|
||||
pnpm run demo:cordis # self-referential demo: the agent modifies its own runtime (needs key)
|
||||
@@ -65,6 +67,7 @@ pnpm run lint
|
||||
pnpm run test:coverage
|
||||
pnpm run test:snapshot
|
||||
pnpm run doc-sync
|
||||
pnpm run website:build
|
||||
pnpm run verify-module-graph
|
||||
pnpm run build
|
||||
pnpm run hygiene
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/knip@5/schema.json",
|
||||
"exclude": ["duplicates"],
|
||||
"ignoreWorkspaces": ["vendor/*"],
|
||||
"ignoreWorkspaces": ["vendor/*", "website"],
|
||||
"workspaces": {
|
||||
".": {
|
||||
"entry": [
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
},
|
||||
"workspaces": [
|
||||
"vendor/*",
|
||||
"packages/*/*"
|
||||
"packages/*/*",
|
||||
"website"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -b tsconfig.build.json && tsdown",
|
||||
@@ -60,6 +61,8 @@
|
||||
"verify-persistence-catalog": "tsx scripts/gen-persistence-catalog.ts --check",
|
||||
"gen-module-graph": "tsx scripts/gen-module-graph.ts",
|
||||
"verify-module-graph": "tsx scripts/gen-module-graph.ts --check",
|
||||
"website:dev": "pnpm --filter @deepseek-ai/website run dev",
|
||||
"website:build": "pnpm --filter @deepseek-ai/website run build",
|
||||
"constraints": "tsx scripts/check-workspace-constraints.ts",
|
||||
"doc-sync": "pnpm run doc-typecheck && pnpm run verify-cordis-catalog && pnpm run verify-cordis-api && pnpm run verify-export-jsdoc && pnpm run verify-tool-catalog && pnpm run verify-config-catalog && pnpm run verify-persistence-catalog && pnpm run verify-doc-graphs && pnpm run verify-md-wrap && pnpm run verify-md-links && pnpm run verify-doc-refs && pnpm run verify-package-paths && pnpm run verify-mermaid && pnpm run verify-rfc-classification && pnpm run verify-rfc-format && pnpm run verify-type-equiv && pnpm run verify-translation-pairing && pnpm run verify-doc-budgets",
|
||||
"hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-node-next-types",
|
||||
@@ -74,12 +77,14 @@
|
||||
"@agentclientprotocol/sdk": "0.25.1",
|
||||
"@stylistic/eslint-plugin": "^5.10.0",
|
||||
"@types/jsdom": "^28.0.3",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@types/node": "^22.20.0",
|
||||
"@vitest/coverage-v8": "^4.1.8",
|
||||
"eslint": "^10.4.1",
|
||||
"fast-check": "^4.8.0",
|
||||
"jsdom": "29.1.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"knip": "^6.16.1",
|
||||
"lefthook": "^2.1.9",
|
||||
"mdast-util-from-markdown": "^2.0.3",
|
||||
|
||||
1448
pnpm-lock.yaml
generated
1448
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
packages:
|
||||
- vendor/*
|
||||
- packages/*/*
|
||||
- website
|
||||
|
||||
peerDependencyRules:
|
||||
allowedVersions:
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
* Doc-sync gate (doc-sync-enforcement RFC, part 1): typecheck the fenced `ts` code blocks in our
|
||||
* Markdown so documentation can't drift from the API it documents.
|
||||
*
|
||||
* Every ```ts block in README.md, docs/** and packages/* /README.md is
|
||||
* Every ```ts block in README.md, docs/**, packages/* /README.md and the
|
||||
* website tutorial pages (website/zh-CN/**) is
|
||||
* extracted to a temp typecheck project and compiled against the workspace
|
||||
* sources through the same project-reference boundaries used by repo
|
||||
* typecheck. A block that is a deliberate sketch rather than compilable code
|
||||
@@ -134,7 +135,7 @@ function tempTsconfig(): string {
|
||||
})
|
||||
}
|
||||
|
||||
const markdownGlobs = ['README.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md']
|
||||
const markdownGlobs = ['README.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md', 'website/zh-CN/**/*.md']
|
||||
|
||||
const files: string[] = []
|
||||
for (const pattern of markdownGlobs) {
|
||||
|
||||
@@ -167,6 +167,7 @@ function ciPrimaryGates(): Gate[] {
|
||||
...docSyncLeafGates(),
|
||||
pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }),
|
||||
pnpmScript('knip', 'knip'),
|
||||
pnpmScript('website-build', 'website:build', { label: 'website build' }),
|
||||
pnpmScript('build', 'build', { needs: ['typecheck'] }),
|
||||
pnpmScript('publint', 'publint', { needs: ['build'] }),
|
||||
pnpmScript('node-next-types', 'verify-node-next-types', {
|
||||
@@ -184,6 +185,7 @@ function ciStaticGates(): Gate[] {
|
||||
...docSyncLeafGates(),
|
||||
pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }),
|
||||
pnpmScript('knip', 'knip'),
|
||||
pnpmScript('website-build', 'website:build', { label: 'website build' }),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ const root = resolve(import.meta.dirname, '..')
|
||||
* added to a doc with NO manifest entry is still discovered here and reported as
|
||||
* an orphan, instead of being silently skipped.
|
||||
*/
|
||||
const MARKDOWN_GLOBS = ['README.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md']
|
||||
const MARKDOWN_GLOBS = ['README.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md', 'website/zh-CN/**/*.md']
|
||||
|
||||
/** One manifest entry: a documented type-equiv block and its source symbol. */
|
||||
interface ManifestEntry {
|
||||
|
||||
@@ -55,16 +55,21 @@ Cordis 同时解决了上述两个问题:
|
||||
|
||||
DeepSeek Harness 将 Cordis 的可组合性应用到 Agent 开发领域:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type {} from '@deepseek-ai/dsh-llm'
|
||||
|
||||
// 一个 Harness 插件天然是可逆的
|
||||
export const inject = ['tools', 'llm'] // 空间可组合:声明依赖
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// 时间可组合:注册会被自动追踪和回收
|
||||
ctx.tools.register(defineTool('my-tool', {
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'my-tool',
|
||||
description: '...',
|
||||
parameters: { /* ... */ },
|
||||
async execute(args) { /* ... */ },
|
||||
async execute(args) { return [] },
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
@@ -50,9 +50,14 @@ Root Context
|
||||
- 因此服务的提供被记录在作用上下文中
|
||||
- 上下文将作用与余作用关联起来,提供了统一的时间、空间可组合性
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
|
||||
// 提供服务 = 一个 effect(占用 ctx.llm 这个 "资源")
|
||||
class LlmService extends Service {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'llm')
|
||||
}
|
||||
// 当此插件卸载时,ctx.llm 被回收(effect 的逆操作)
|
||||
// 所有依赖 llm 的插件因 coeffect 不满足而挂起
|
||||
}
|
||||
@@ -66,7 +71,16 @@ class LlmService extends Service {
|
||||
|
||||
框架将领域中的所有方法都封装为 effect 版本。开发者只需调用 `ctx` 上的方法,就能自动获得时间/空间可组合性:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
|
||||
import type { LlmAdapter, Message } from '@deepseek-ai/dsh-llm'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
|
||||
declare function validateResult(agent: Agent, turn: number, step: number, message: Message, next: () => Promise<Message>): Promise<Message>
|
||||
declare const myTool: ToolDefinition
|
||||
declare const adapter: LlmAdapter
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// 以下每一行都是 effect——卸载时自动逆序回收
|
||||
ctx.on('agent/step-result', validateResult)
|
||||
@@ -82,7 +96,16 @@ export function apply(ctx: Context) {
|
||||
|
||||
可以逐步将现有框架中的 API 替换为可组合版本,无需一次性重写:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
declare const ctx: Context
|
||||
declare function handler(): void
|
||||
declare const legacySystem: {
|
||||
register(handler: () => void): object
|
||||
unregister(token: object): void
|
||||
}
|
||||
|
||||
// 第一步:用 ctx.effect 包装遗留 API
|
||||
ctx.effect(() => {
|
||||
const legacy = legacySystem.register(handler)
|
||||
|
||||
@@ -24,13 +24,19 @@ Cordis 将程序中的资源依赖抽象为**服务** (service):
|
||||
- 运行时对依赖不满足的插件**等待**,而非拒绝
|
||||
- 服务生命周期结束前,依赖该服务的插件**先一步被回收**
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
|
||||
// LLM 适配器插件:提供 llm 服务
|
||||
export class LlmService extends Service {
|
||||
static inject = ['http'] // 自身依赖 http
|
||||
// 当 http 不可用时,LlmService 自动挂起
|
||||
// 挂起导致 ctx.llm 不可用
|
||||
// 所有 inject: ['llm'] 的插件级联挂起
|
||||
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'llm')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -57,7 +63,11 @@ export class LlmService extends Service {
|
||||
|
||||
## 在 Cordis 中的实现
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
import type {} from '@deepseek-ai/dsh-llm'
|
||||
|
||||
// 声明依赖
|
||||
export const inject = ['tools', 'llm']
|
||||
|
||||
@@ -85,6 +95,6 @@ llm service 恢复 → 依赖 llm 的插件重新 PENDING → ACTIVE
|
||||
| LLM adapter 热替换 | 依赖 `llm` 的插件自动挂起/恢复,中间不丢状态 |
|
||||
| 按需加载 bash 执行器 | bash tool 只在 `bash` 服务就绪后注册 |
|
||||
| 子 Agent 独立服务空间 | 通过 `ctx.isolate()` 隔离服务实例,互不干扰 |
|
||||
| 可选能力降级 | `inject: { web: { required: false } }` 允许 web 不可用时继续运行 |
|
||||
| 可选能力降级 | 不声明 `inject`,用 `ctx.get('web')` 读取——服务不可用时返回 `undefined`,插件照常运行 |
|
||||
|
||||
这意味着 Harness 插件开发者无需编写防御性的 "if service exists" 检查——框架保证:当你的 `apply` 被调用时,声明的依赖一定已就绪。
|
||||
|
||||
@@ -103,7 +103,20 @@ $$
|
||||
| $\text{restore}$ | `fiber.dispose()` | 执行 Fiber 的整个回收链 |
|
||||
| $f^{-1}$ | dispose 返回值 / cleanup 函数 | 逆操作 |
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Events {
|
||||
'my-plugin/event'(): void
|
||||
}
|
||||
}
|
||||
|
||||
declare function startServer(port: number): { close(): void }
|
||||
declare function handler(): void
|
||||
declare const myTool: ToolDefinition
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// effect: 创建资源,返回其逆操作
|
||||
ctx.effect(() => {
|
||||
@@ -112,7 +125,7 @@ export function apply(ctx: Context) {
|
||||
})
|
||||
|
||||
// 框架 API 内部已封装 effect
|
||||
ctx.on('event', handler) // 内部: effect(addListener, removeListener)
|
||||
ctx.on('my-plugin/event', handler) // 内部: effect(addListener, removeListener)
|
||||
ctx.tools.register(myTool) // 内部: effect(addTool, removeTool)
|
||||
}
|
||||
// 当此插件被卸载时,restore 自动按逆序执行所有 f⁻¹
|
||||
|
||||
@@ -4,27 +4,21 @@
|
||||
|
||||
## 定义 Config 类型
|
||||
|
||||
在插件中导出一个 `Config` 类型和可选的默认值:
|
||||
在插件中导出一个 `Config` 类型,`apply` 的第二个参数就是用户配置:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
export const name = 'my-plugin'
|
||||
|
||||
export interface Config {
|
||||
greeting: string
|
||||
maxRetries: number
|
||||
greeting?: string
|
||||
maxRetries?: number
|
||||
verbose?: boolean
|
||||
}
|
||||
|
||||
export const Config = {
|
||||
greeting: 'Hello',
|
||||
maxRetries: 3,
|
||||
verbose: false,
|
||||
}
|
||||
|
||||
export function apply(ctx: Context, config: Config) {
|
||||
console.log(config.greeting) // 用户配置或默认值
|
||||
console.log(config.greeting ?? 'Hello') // 用户配置或默认值
|
||||
}
|
||||
```
|
||||
|
||||
@@ -37,32 +31,32 @@ export function apply(ctx: Context, config: Config) {
|
||||
maxRetries: 5
|
||||
```
|
||||
|
||||
未提供的字段使用导出的 `Config` 对象中的默认值。
|
||||
只导出类型时,配置原样传入,默认值由代码自己兜底(如上面的 `??`)。想让框架代管默认值和校验,导出一个 schema(见下节)。
|
||||
|
||||
## Schema 校验
|
||||
|
||||
对于需要严格校验的场景,使用 Schemastery 定义 schema:
|
||||
对于需要默认值和严格校验的场景,额外导出一个 Schemastery schema(仓库约定以 `z` 引入)。加载时框架先用它校验并填充默认值,再把结果传给 `apply`:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import Schema from 'schemastery'
|
||||
import z from 'schemastery'
|
||||
|
||||
export const name = 'validated-plugin'
|
||||
|
||||
export interface Config {
|
||||
apiKey: string
|
||||
timeout: number
|
||||
mode: 'fast' | 'accurate'
|
||||
timeout?: number
|
||||
mode?: 'fast' | 'accurate'
|
||||
}
|
||||
|
||||
export const Config = Schema.object({
|
||||
apiKey: Schema.string().required(),
|
||||
timeout: Schema.number().default(30000),
|
||||
mode: Schema.union(['fast', 'accurate']).default('fast'),
|
||||
export const Config: z<Config> = z.object({
|
||||
apiKey: z.string().required(),
|
||||
timeout: z.number().default(30000),
|
||||
mode: z.union(['fast', 'accurate'] as const).default('fast'),
|
||||
})
|
||||
|
||||
export function apply(ctx: Context, config: Config) {
|
||||
// config 已经过校验,类型安全
|
||||
// config 已经过校验,类型安全,默认值已填充
|
||||
}
|
||||
```
|
||||
|
||||
@@ -74,13 +68,14 @@ Schema 在插件加载时执行校验。如果配置不合法,插件会加载
|
||||
|
||||
Harness 的约定:**任何两个部署可能想要不同值的东西,都应该是配置字段**。
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
// 错误 — 硬编码超时时间
|
||||
const TIMEOUT = 30000
|
||||
|
||||
// 正确 — 可配置
|
||||
export interface Config {
|
||||
timeoutMs: number // 默认 30000
|
||||
/** 默认 30000 */
|
||||
timeoutMs?: number
|
||||
}
|
||||
```
|
||||
|
||||
@@ -90,9 +85,16 @@ export interface Config {
|
||||
|
||||
如果配置引用了不存在的东西(比如一个不存在的模型名),应该尽早报错,而不是静默跳过:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import type {} from '@deepseek-ai/dsh-llm'
|
||||
|
||||
export interface Config {
|
||||
model: string
|
||||
}
|
||||
|
||||
export function apply(ctx: Context, config: Config) {
|
||||
if (!ctx.llm.hasAdapter(config.model)) {
|
||||
if (!ctx.llm.models().includes(config.model)) {
|
||||
throw new Error(`Model "${config.model}" is not registered by any LLM adapter`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
在 Harness 中,插件是一个导出 `apply` 函数的 TypeScript 模块。框架在加载时调用 `apply`,传入一个 `ctx`(上下文对象),你通过 `ctx` 注册能力:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
export const name = 'my-plugin'
|
||||
@@ -22,16 +22,14 @@ export function apply(ctx: Context) {
|
||||
|
||||
在你的项目目录下创建 `src/my-plugin.ts`:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
export const name = 'hello-plugin'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// 监听 agent-loop 的 ready 事件
|
||||
ctx.on('ready', () => {
|
||||
console.log('[hello-plugin] 插件已加载!')
|
||||
})
|
||||
// apply 函数体在插件加载时执行
|
||||
console.log('[hello-plugin] 插件已加载!')
|
||||
}
|
||||
```
|
||||
|
||||
@@ -52,7 +50,9 @@ export function apply(ctx: Context) {
|
||||
|
||||
如果你有需要手动清理的资源(比如一个网络连接),用 `ctx.effect()` 告诉框架怎么清理:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.effect(() => {
|
||||
const timer = setInterval(() => {
|
||||
@@ -69,13 +69,23 @@ export function apply(ctx: Context) {
|
||||
|
||||
如果你的插件需要使用其他服务(如 `tools`、`llm`),需要声明 `inject`:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const name = 'my-tool-plugin'
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// ctx.tools 现在可用
|
||||
ctx.tools.register(/* ... */)
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'demo',
|
||||
description: 'Demo tool.',
|
||||
parameters: {},
|
||||
async execute() {
|
||||
return []
|
||||
},
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
@@ -87,7 +97,10 @@ export function apply(ctx: Context) {
|
||||
|
||||
### 对象形式
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export default {
|
||||
name: 'my-plugin',
|
||||
inject: ['tools'],
|
||||
@@ -99,8 +112,9 @@ export default {
|
||||
|
||||
### 类形式
|
||||
|
||||
```typescript
|
||||
import { Service } from 'cordis'
|
||||
```ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export default class MyService extends Service {
|
||||
static inject = ['tools']
|
||||
@@ -109,8 +123,9 @@ export default class MyService extends Service {
|
||||
super(ctx, 'myService')
|
||||
}
|
||||
|
||||
start() {
|
||||
// 服务启动逻辑
|
||||
// 服务的公开方法
|
||||
greet(name: string) {
|
||||
return `Hello, ${name}!`
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -121,7 +136,7 @@ export default class MyService extends Service {
|
||||
|
||||
参考仓库中的 `examples/echo-agent/src/echo-tool.ts`,这是一个注册 tool 的插件:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ Tool 是模型可以调用的能力。本文介绍如何用 `defineTool` 编写
|
||||
|
||||
## 最小示例
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
@@ -32,28 +32,34 @@ export function apply(ctx: Context) {
|
||||
|
||||
### 基本类型
|
||||
|
||||
```typescript
|
||||
parameters: {
|
||||
```ts
|
||||
import type { SchemaSpec } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
const parameters = {
|
||||
path: { type: 'string', required: true },
|
||||
limit: { type: 'number' },
|
||||
recursive: { type: 'boolean' },
|
||||
}
|
||||
} satisfies SchemaSpec
|
||||
// 推导类型: { path: string; limit?: number; recursive?: boolean }
|
||||
```
|
||||
|
||||
### 枚举
|
||||
|
||||
```typescript
|
||||
parameters: {
|
||||
```ts
|
||||
import type { SchemaSpec } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
const parameters = {
|
||||
mode: { type: 'string', required: true, enum: ['read', 'write', 'append'] },
|
||||
}
|
||||
} satisfies SchemaSpec
|
||||
// 推导类型: { mode: string } (运行时校验 enum 值)
|
||||
```
|
||||
|
||||
### 嵌套对象
|
||||
|
||||
```typescript
|
||||
parameters: {
|
||||
```ts
|
||||
import type { SchemaSpec } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
const parameters = {
|
||||
options: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -61,19 +67,21 @@ parameters: {
|
||||
retries: { type: 'number' },
|
||||
},
|
||||
},
|
||||
}
|
||||
} satisfies SchemaSpec
|
||||
// 推导类型: { options?: { timeout?: number; retries?: number } }
|
||||
```
|
||||
|
||||
### 数组
|
||||
|
||||
```typescript
|
||||
parameters: {
|
||||
```ts
|
||||
import type { SchemaSpec } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
const parameters = {
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
}
|
||||
} satisfies SchemaSpec
|
||||
// 推导类型: { tags?: string[] }
|
||||
```
|
||||
|
||||
@@ -92,29 +100,44 @@ parameters: {
|
||||
|
||||
`execute` 接收经过校验的 `args`(类型自动推导)和一个 `exec` 上下文对象:
|
||||
|
||||
```typescript
|
||||
async execute(args, exec) {
|
||||
// args: 根据 parameters 自动推导的类型
|
||||
// exec: ToolExecution 对象,提供执行上下文
|
||||
```ts
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
// 返回 ContentBlock 数组
|
||||
return [{ type: 'text', text: 'result here' }]
|
||||
}
|
||||
defineTool({
|
||||
name: 'demo',
|
||||
description: 'Demo tool.',
|
||||
parameters: {},
|
||||
async execute(args, exec) {
|
||||
// args: 根据 parameters 自动推导的类型
|
||||
// exec: ToolExecution 对象,提供执行上下文
|
||||
|
||||
// 返回 ContentBlock 数组
|
||||
return [{ type: 'text', text: 'result here' }]
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### 返回值
|
||||
|
||||
`execute` 必须返回一个 `ContentBlock[]`,告诉模型 tool 的执行结果:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
declare const matchResults: string[]
|
||||
|
||||
// 文本结果
|
||||
return [{ type: 'text', text: 'file content here...' }]
|
||||
function textResult(): ContentBlock[] {
|
||||
return [{ type: 'text', text: 'file content here...' }]
|
||||
}
|
||||
|
||||
// 多个 block
|
||||
return [
|
||||
{ type: 'text', text: 'Found 3 matches:' },
|
||||
{ type: 'text', text: matchResults.join('\n') },
|
||||
]
|
||||
function multiBlockResult(): ContentBlock[] {
|
||||
return [
|
||||
{ type: 'text', text: 'Found 3 matches:' },
|
||||
{ type: 'text', text: matchResults.join('\n') },
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 参数校验
|
||||
@@ -127,20 +150,28 @@ return [
|
||||
|
||||
Tool 可以定义 UI 渲染方法,用于在终端或 ACP 客户端中展示 tool call 和 result:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
defineTool({
|
||||
name: 'bash',
|
||||
// ...
|
||||
description: 'Run a shell command.',
|
||||
parameters: {
|
||||
command: { type: 'string', required: true },
|
||||
},
|
||||
async execute(args) {
|
||||
return [{ type: 'text', text: `ran: ${args.command}` }]
|
||||
},
|
||||
presentCall(args) {
|
||||
return {
|
||||
intent: 'terminal',
|
||||
title: `bash(${JSON.stringify(args.command).slice(0, 60)})`,
|
||||
card: 'terminal',
|
||||
title: args.command.slice(0, 60),
|
||||
}
|
||||
},
|
||||
presentResult(args, result) {
|
||||
return {
|
||||
intent: 'terminal',
|
||||
body: result.content.map(b => b.type === 'text' ? b.text : '').join(''),
|
||||
card: 'terminal',
|
||||
output: result.content.map(b => b.type === 'text' ? b.text : '').join(''),
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -152,20 +183,32 @@ defineTool({
|
||||
|
||||
`ctx.tools.register()` 返回值就是 disposer。但由于你在 `ctx` 上调用,框架已经自动追踪了这个注册——插件卸载时会自动移除 tool。你不需要手动调用 disposer。
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
declare const ctx: Context
|
||||
|
||||
// 这样就够了:
|
||||
ctx.tools.register(defineTool({ /* ... */ }))
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'noop',
|
||||
description: 'Do nothing.',
|
||||
parameters: {},
|
||||
async execute() {
|
||||
return []
|
||||
},
|
||||
}))
|
||||
|
||||
// 不需要:
|
||||
// const dispose = ctx.tools.register(...)
|
||||
// ctx.on('dispose', dispose)
|
||||
// ctx.effect(() => dispose)
|
||||
```
|
||||
|
||||
## 完整实战示例
|
||||
|
||||
一个文件计数 tool:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import { readdir } from 'node:fs/promises'
|
||||
|
||||
@@ -6,7 +6,17 @@
|
||||
|
||||
### 监听事件
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Events {
|
||||
'event-name'(payload: string): void
|
||||
}
|
||||
}
|
||||
|
||||
declare const ctx: Context
|
||||
|
||||
ctx.on('event-name', (payload) => {
|
||||
// 处理事件
|
||||
})
|
||||
@@ -14,7 +24,18 @@ ctx.on('event-name', (payload) => {
|
||||
|
||||
### 触发事件
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Events {
|
||||
'event-name'(payload: string): void
|
||||
}
|
||||
}
|
||||
|
||||
declare const ctx: Context
|
||||
declare const payload: string
|
||||
|
||||
ctx.emit('event-name', payload)
|
||||
```
|
||||
|
||||
@@ -26,12 +47,24 @@ Cordis 提供多种事件触发模式,适用于不同场景:
|
||||
|
||||
所有监听器并行执行,不关心返回值:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Events {
|
||||
'my-plugin/turn-end'(agentId: string, turnIndex: number): void
|
||||
}
|
||||
}
|
||||
|
||||
declare const ctx: Context
|
||||
declare const agentId: string
|
||||
declare const turnIndex: number
|
||||
|
||||
// 触发
|
||||
ctx.emit('agent/turn-end', { agentId, turnIndex })
|
||||
ctx.emit('my-plugin/turn-end', agentId, turnIndex)
|
||||
|
||||
// 监听
|
||||
ctx.on('agent/turn-end', ({ agentId, turnIndex }) => {
|
||||
ctx.on('my-plugin/turn-end', (agentId, turnIndex) => {
|
||||
console.log(`Turn ${turnIndex} ended`)
|
||||
})
|
||||
```
|
||||
@@ -40,7 +73,19 @@ ctx.on('agent/turn-end', ({ agentId, turnIndex }) => {
|
||||
|
||||
依次调用监听器,第一个返回非 `undefined` 值的结果作为最终值:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Events {
|
||||
'some-check'(input: string): string | undefined
|
||||
}
|
||||
}
|
||||
|
||||
declare const ctx: Context
|
||||
declare const input: string
|
||||
declare function shouldBlock(input: string): boolean
|
||||
|
||||
// 触发
|
||||
const result = ctx.bail('some-check', input)
|
||||
|
||||
@@ -48,6 +93,7 @@ const result = ctx.bail('some-check', input)
|
||||
ctx.on('some-check', (input) => {
|
||||
if (shouldBlock(input)) return 'blocked'
|
||||
// 返回 undefined 继续传递给下一个监听器
|
||||
return undefined
|
||||
})
|
||||
```
|
||||
|
||||
@@ -55,24 +101,47 @@ ctx.on('some-check', (input) => {
|
||||
|
||||
所有监听器按注册顺序依次执行(异步安全):
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Events {
|
||||
'setup-phase'(context: object): Promise<void> | void
|
||||
}
|
||||
}
|
||||
|
||||
declare const ctx: Context
|
||||
declare const context: object
|
||||
|
||||
await ctx.serial('setup-phase', context)
|
||||
```
|
||||
|
||||
### waterfall — 管道
|
||||
|
||||
每个监听器接收前一个的输出,形成数据管道。**必须调用 `next()` 传递给下游**,不调用即为否决:
|
||||
监听器围绕默认实现层层包裹,形成数据管道。**必须调用 `next()` 委托给下游**,不调用即为否决:
|
||||
|
||||
```typescript
|
||||
// 触发
|
||||
const finalMessages = await ctx.waterfall('llm/pre-request', messages)
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import type { Message } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Events {
|
||||
'my-plugin/messages'(messages: Message[], next: () => Promise<Message[]>): Promise<Message[]>
|
||||
}
|
||||
}
|
||||
|
||||
declare const ctx: Context
|
||||
declare const messages: Message[]
|
||||
declare const extraMessage: Message
|
||||
|
||||
// 触发:最后一个参数是默认实现(所有监听器都调用 next 时的最终值)
|
||||
const finalMessages = await ctx.waterfall('my-plugin/messages', messages, async () => messages)
|
||||
|
||||
// 监听(必须调用 next)
|
||||
ctx.on('llm/pre-request', async (messages, next) => {
|
||||
// 可以修改 messages
|
||||
messages.push(extraMessage)
|
||||
// 必须调用 next() 传递给下一个监听器
|
||||
return next(messages)
|
||||
ctx.on('my-plugin/messages', async (messages, next) => {
|
||||
// next() 委托给下游监听器(最终到达默认实现),返回值可以被加工
|
||||
const result = await next()
|
||||
return [...result, extraMessage]
|
||||
})
|
||||
```
|
||||
|
||||
@@ -84,11 +153,13 @@ Waterfall 监听器**必须调用 `next()`**。不调用 `next` 等于否决整
|
||||
|
||||
Harness 使用 TypeScript 声明合并来为事件提供类型安全:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type {} from 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Events {
|
||||
'my-plugin/ready': (payload: { id: string }) => void
|
||||
'my-plugin/check': (input: string) => boolean | undefined
|
||||
'my-plugin/ready'(payload: { id: string }): void
|
||||
'my-plugin/check'(input: string): boolean | undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,24 +172,30 @@ declare module 'cordis' {
|
||||
Harness 事件遵循 `namespace/action` 命名:
|
||||
|
||||
```
|
||||
agent/pre-step — agent 执行一步之前
|
||||
agent/post-step — agent 执行一步之后
|
||||
tool/call — tool 被调用
|
||||
tool/result — tool 返回结果
|
||||
llm/pre-request — LLM 请求发送前
|
||||
session/event — 会话事件被记录
|
||||
compact/start — 压缩开始
|
||||
compact/end — 压缩结束
|
||||
agent/pre-step — 每个 step 开始前的检查点(serial)
|
||||
agent/step-result — step 的 assistant 消息组装完成(waterfall)
|
||||
tools/pre-execute — tool 执行前的允许/拒绝门(waterfall)
|
||||
tools/post-execute — tool 执行后的检查/改写缝(waterfall)
|
||||
llm/stream — 每次流式模型调用的环绕点(waterfall)
|
||||
session/event — 会话事件被记录(emit)
|
||||
session/flush — 会话持久化检查点(parallel)
|
||||
```
|
||||
|
||||
完整的事件列表(含每个事件的签名与派发模式)见仓库中的 `docs/cordis-catalog/events.md`。
|
||||
|
||||
## 事件也是效果
|
||||
|
||||
通过 `ctx.on()` 注册的监听器会在插件卸载时自动移除:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent'
|
||||
|
||||
declare function handler(agent: Agent, status: AgentStatus): void
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// 这个监听器在插件 dispose 时自动清理
|
||||
ctx.on('agent/turn-end', handler)
|
||||
ctx.on('agent/status', handler)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -126,22 +203,21 @@ export function apply(ctx: Context) {
|
||||
|
||||
一个记录所有 tool 调用的简单插件:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const name = 'tool-logger'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.on('tool/call', ({ name, args }) => {
|
||||
console.log(`[tool] ${name}(${JSON.stringify(args)})`)
|
||||
})
|
||||
|
||||
ctx.on('tool/result', ({ name, result }) => {
|
||||
ctx.on('tools/execute', async (exec, next) => {
|
||||
console.log(`[tool] ${exec.name}(${JSON.stringify(exec.arguments)})`)
|
||||
const result = await next()
|
||||
const text = result.content
|
||||
.filter(b => b.type === 'text')
|
||||
.map(b => b.text)
|
||||
.map(b => b.type === 'text' ? b.text : '')
|
||||
.join('')
|
||||
console.log(`[tool result] ${text.slice(0, 100)}`)
|
||||
return result
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
@@ -25,7 +25,11 @@ ACTIVE → UNLOADING → DISPOSED
|
||||
|
||||
声明了 `inject` 的插件不会立即加载,而是等待依赖的服务就绪:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
import type {} from '@deepseek-ai/dsh-llm'
|
||||
|
||||
export const inject = ['tools', 'llm']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
@@ -39,10 +43,21 @@ export function apply(ctx: Context) {
|
||||
|
||||
通过 `ctx` 做的任何注册,在插件卸载时都会自动撤销:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Events {
|
||||
'my-plugin/some-event'(): void
|
||||
}
|
||||
}
|
||||
|
||||
declare function handler(): void
|
||||
declare function createConnection(): { close(): void }
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// 事件监听——卸载时自动移除
|
||||
ctx.on('some-event', handler)
|
||||
ctx.on('my-plugin/some-event', handler)
|
||||
|
||||
// 自定义资源——卸载时调用返回的函数
|
||||
ctx.effect(() => {
|
||||
@@ -64,7 +79,11 @@ export function apply(ctx: Context) {
|
||||
|
||||
`ctx.plugin()` 创建子 Fiber,它继承父上下文但有独立的生命周期:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
declare function childPlugin(ctx: Context): void
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// 注册一个子插件
|
||||
ctx.plugin(childPlugin)
|
||||
@@ -77,11 +96,16 @@ export function apply(ctx: Context) {
|
||||
|
||||
当你需要提前终止一个插件实例:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
declare const ctx: Context
|
||||
declare function myPlugin(ctx: Context): void
|
||||
|
||||
const fiber = ctx.plugin(myPlugin)
|
||||
|
||||
// 之后可以手动 dispose
|
||||
fiber.dispose()
|
||||
await fiber.dispose()
|
||||
```
|
||||
|
||||
`dispose` 保证:
|
||||
@@ -101,18 +125,14 @@ fiber.dispose()
|
||||
|
||||
## 实战:理解生命周期
|
||||
|
||||
```typescript
|
||||
`apply` 函数体就是加载钩子;卸载没有专门的事件——把清理逻辑放进 `ctx.effect()` 的返回函数即可:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
console.log('plugin loading')
|
||||
|
||||
ctx.on('ready', () => {
|
||||
console.log('context ready')
|
||||
})
|
||||
|
||||
ctx.on('dispose', () => {
|
||||
console.log('plugin disposing')
|
||||
})
|
||||
|
||||
ctx.effect(() => {
|
||||
console.log('effect registered')
|
||||
return () => console.log('effect cleaned up')
|
||||
@@ -124,12 +144,10 @@ export function apply(ctx: Context) {
|
||||
```
|
||||
plugin loading
|
||||
effect registered
|
||||
context ready
|
||||
```
|
||||
|
||||
卸载时输出(逆序):
|
||||
卸载时输出:
|
||||
```
|
||||
plugin disposing
|
||||
effect cleaned up
|
||||
```
|
||||
|
||||
|
||||
@@ -6,10 +6,17 @@
|
||||
|
||||
在 Harness 中,`tools`、`llm`、`agents` 都是服务。服务是挂载在 `ctx` 上的命名能力:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
import type {} from '@deepseek-ai/dsh-llm'
|
||||
import type {} from '@deepseek-ai/dsh-agent'
|
||||
|
||||
declare const ctx: Context
|
||||
|
||||
ctx.tools // ToolRegistry 服务
|
||||
ctx.llm // LLM 服务
|
||||
ctx.agents // Agent 服务
|
||||
ctx.agents // Agent 注册表服务
|
||||
```
|
||||
|
||||
任何插件都可以提供一个新服务,供其他插件使用。
|
||||
@@ -18,12 +25,22 @@ ctx.agents // Agent 服务
|
||||
|
||||
声明 `inject` 来使用已有服务:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// ctx.tools 在这里一定存在且就绪
|
||||
ctx.tools.register(/* ... */)
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'demo',
|
||||
description: 'Demo tool.',
|
||||
parameters: {},
|
||||
async execute() {
|
||||
return []
|
||||
},
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
@@ -33,8 +50,9 @@ export function apply(ctx: Context) {
|
||||
|
||||
### 使用 Service 基类
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
import type {} from '@deepseek-ai/dsh-llm'
|
||||
|
||||
export default class MetricsService extends Service {
|
||||
static inject = ['llm'] // 本服务也可以依赖其他服务
|
||||
@@ -52,7 +70,9 @@ export default class MetricsService extends Service {
|
||||
|
||||
加载这个插件后,其他插件就可以通过 `ctx.metrics` 访问它:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
export const inject = ['metrics']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
@@ -64,7 +84,7 @@ export function apply(ctx: Context) {
|
||||
|
||||
使用 TypeScript 声明合并让 `ctx.metrics` 有正确类型:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
@@ -84,14 +104,21 @@ export default class MetricsService extends Service {
|
||||
|
||||
## 依赖的行为
|
||||
|
||||
### 必选依赖 vs 可选依赖
|
||||
### 必选依赖 vs 可选读取
|
||||
|
||||
`inject` 声明的依赖都是必选的:服务不存在时,插件不会加载。如果只想"有则用之",用 `ctx.get()` 读取——服务不存在时返回 `undefined`,插件照常加载:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
```typescript
|
||||
// 必选:服务不存在时,插件不会加载
|
||||
export const inject = ['tools']
|
||||
|
||||
// 可选:服务不存在时,插件仍然加载,但 ctx.xxx 可能是 undefined
|
||||
export const inject = { optional: ['metrics'] }
|
||||
export function apply(ctx: Context) {
|
||||
// 可选读取:不声明 inject,服务不存在时返回 undefined
|
||||
const metrics = ctx.get('metrics')
|
||||
metrics?.record('plugin_loaded', 1)
|
||||
}
|
||||
```
|
||||
|
||||
### 服务消失时的行为
|
||||
@@ -133,13 +160,14 @@ export const inject = { optional: ['metrics'] }
|
||||
|--------|--------|------|
|
||||
| `tools` | dsh-tools | Tool 注册表 |
|
||||
| `llm` | dsh-llm | LLM 调用 + 适配器注册 |
|
||||
| `agents` | dsh-agent | Agent 实例管理 |
|
||||
| `session` | dsh-session | 会话事件流 |
|
||||
| `agents` | dsh-agent | Agent 注册表 |
|
||||
| `agentLoop` | dsh-agent-loop | Agent 创建与循环执行 |
|
||||
| `sessions` | dsh-session | 会话存储与事件流 |
|
||||
| `systemPrompt` | dsh-system-prompt | 系统提示词组装 |
|
||||
| `bash` | dsh-bash-local | Bash 命令执行 |
|
||||
| `fs` | dsh-fs-local | 文件系统操作 |
|
||||
| `subagent` | dsh-subagent | 子代理委派 |
|
||||
| `persistence` | dsh-session-persistence | 会话持久化 |
|
||||
| `bash` | dsh-bash(实现:dsh-bash-local) | Bash 命令执行 |
|
||||
| `fs` | dsh-fs(实现:dsh-fs-local) | 文件系统操作 |
|
||||
| `subagents` | dsh-subagent | 子代理委派 |
|
||||
| `sessionPersistence` | dsh-session-persistence(实现:-jsonl / -sqlite) | 会话持久化 |
|
||||
|
||||
## 下一步
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
|
||||
### 第一步:定义接口
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
// packages/my-cap/my-cap/src/index.ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
|
||||
@@ -94,7 +94,7 @@ export interface MyCapResult {
|
||||
|
||||
### 第二步:编写实现
|
||||
|
||||
```typescript
|
||||
```ts ignore-check
|
||||
// packages/my-cap/my-cap-local/src/index.ts
|
||||
import type { Context } from 'cordis'
|
||||
import { MyCapService, type MyCapRequest, type MyCapResult } from '@deepseek-ai/dsh-my-cap'
|
||||
@@ -115,7 +115,7 @@ export function apply(ctx: Context) {
|
||||
|
||||
### 第三步:编写消费者 (tool)
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
// packages/my-cap/tool-my-cap/src/index.ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
@@ -8,7 +8,7 @@ LLM 适配器是一个继承 `LlmAdapter` 的类,实现 `stream()` 方法,
|
||||
|
||||
## 最小实现
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
@@ -45,47 +45,51 @@ export function apply(ctx: Context, config: Config) {
|
||||
|
||||
`stream()` 必须按以下协议 yield chunk:
|
||||
|
||||
```typescript
|
||||
// 1. 每个内容块以 block-start 开始
|
||||
yield { type: 'block-start', index: 0, blockType: 'text' }
|
||||
```ts
|
||||
import { CallId, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
// 2. 文本块使用 text-delta
|
||||
yield { type: 'text-delta', index: 0, text: 'Hello' }
|
||||
yield { type: 'text-delta', index: 0, text: ' world' }
|
||||
async function* demo(): AsyncIterable<StreamChunk> {
|
||||
// 1. 每个内容块以 block-start 开始
|
||||
yield { type: 'block-start', index: 0, blockType: 'text' }
|
||||
|
||||
// 3. 每个内容块以 block-end 结束(携带完整 block)
|
||||
yield {
|
||||
type: 'block-end',
|
||||
index: 0,
|
||||
block: { type: 'text', text: 'Hello world' },
|
||||
}
|
||||
// 2. 文本块使用 text-delta
|
||||
yield { type: 'text-delta', index: 0, text: 'Hello' }
|
||||
yield { type: 'text-delta', index: 0, text: ' world' }
|
||||
|
||||
// 4. Tool call 块
|
||||
yield { type: 'block-start', index: 1, blockType: 'tool-call' }
|
||||
yield {
|
||||
type: 'tool-call-delta',
|
||||
index: 1,
|
||||
id: CallId('call-123'),
|
||||
name: 'bash',
|
||||
argumentsDelta: '{"command":"ls"}',
|
||||
}
|
||||
yield {
|
||||
type: 'block-end',
|
||||
index: 1,
|
||||
block: {
|
||||
type: 'tool-call',
|
||||
// 3. 每个内容块以 block-end 结束(携带完整 block)
|
||||
yield {
|
||||
type: 'block-end',
|
||||
index: 0,
|
||||
block: { type: 'text', text: 'Hello world' },
|
||||
}
|
||||
|
||||
// 4. Tool call 块
|
||||
yield { type: 'block-start', index: 1, blockType: 'tool-call' }
|
||||
yield {
|
||||
type: 'tool-call-delta',
|
||||
index: 1,
|
||||
id: CallId('call-123'),
|
||||
name: 'bash',
|
||||
arguments: '{"command":"ls"}',
|
||||
},
|
||||
argumentsDelta: '{"command":"ls"}',
|
||||
}
|
||||
yield {
|
||||
type: 'block-end',
|
||||
index: 1,
|
||||
block: {
|
||||
type: 'tool-call',
|
||||
id: CallId('call-123'),
|
||||
name: 'bash',
|
||||
arguments: '{"command":"ls"}',
|
||||
},
|
||||
}
|
||||
|
||||
// 5. Token 用量
|
||||
yield { type: 'usage', usage: { inputTokens: 100, outputTokens: 50 } }
|
||||
|
||||
// 6. 结束原因
|
||||
yield { type: 'finish', reason: { kind: 'stop' } }
|
||||
// 或: { kind: 'tool-calls' } 表示模型想调用 tool
|
||||
}
|
||||
|
||||
// 5. Token 用量
|
||||
yield { type: 'usage', usage: { inputTokens: 100, outputTokens: 50 } }
|
||||
|
||||
// 6. 结束原因
|
||||
yield { type: 'finish', reason: { kind: 'stop' } }
|
||||
// 或: { kind: 'tool-calls' } 表示模型想调用 tool
|
||||
```
|
||||
|
||||
### 关键规则
|
||||
@@ -100,28 +104,31 @@ yield { type: 'finish', reason: { kind: 'stop' } }
|
||||
|
||||
`stream()` 接收的请求包含:
|
||||
|
||||
```typescript
|
||||
interface GenerateOptions {
|
||||
/** 模型名 */
|
||||
model: string
|
||||
/** 对话历史 */
|
||||
messages: Message[]
|
||||
/** 可用的 tool 列表 */
|
||||
tools?: ToolSpec[]
|
||||
/** 系统提示词 */
|
||||
system?: string
|
||||
/** 最大输出 token */
|
||||
maxTokens?: number
|
||||
/** 温度 */
|
||||
temperature?: number
|
||||
}
|
||||
```ts
|
||||
import type { GenerateOptions } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
declare const options: GenerateOptions
|
||||
|
||||
options.model // 模型名
|
||||
options.messages // 对话历史 (Message[])
|
||||
options.tools // 可用的 tool schema 列表 (ToolSchema[])
|
||||
options.system // 系统提示词
|
||||
options.maxTokens // 最大输出 token
|
||||
options.temperature // 温度
|
||||
options.signal // 取消信号(必须响应)
|
||||
```
|
||||
|
||||
你的适配器需要将这些映射到具体 API 的参数。
|
||||
|
||||
## 注册适配器
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import type { LlmAdapter } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
declare const ctx: Context
|
||||
declare const adapter: LlmAdapter
|
||||
|
||||
ctx.llm.registerAdapter(['model-name-1', 'model-name-2'], adapter)
|
||||
```
|
||||
|
||||
@@ -158,12 +165,18 @@ mock 适配器是学习 StreamChunk 协议的最佳起点——它用纯本地
|
||||
|
||||
适配器中的异常会被 agent-loop 捕获并转化为 `LlmError`,告知上层。不需要在 `stream()` 内部做错误恢复——让异常冒泡即可。
|
||||
|
||||
```typescript
|
||||
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
||||
const response = await fetch(this.endpoint, { /* ... */ })
|
||||
if (!response.ok) {
|
||||
throw new Error(`API error: ${response.status}`)
|
||||
```ts
|
||||
import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
class HttpAdapter extends LlmAdapter {
|
||||
private endpoint = 'https://api.example.com/v1/chat'
|
||||
|
||||
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
||||
const response = await fetch(this.endpoint, { method: 'POST' })
|
||||
if (!response.ok) {
|
||||
throw new Error(`API error: ${response.status}`)
|
||||
}
|
||||
// ... 正常流式处理
|
||||
}
|
||||
// ... 正常流式处理
|
||||
}
|
||||
```
|
||||
|
||||
@@ -87,6 +87,7 @@ Harness 使用 `cordis.yml` 描述一个 Agent 加载哪些插件、以什么参
|
||||
# 自动压缩:对话太长时自动总结旧内容,腾出上下文空间
|
||||
# contextWindow 是模型能看到的 token 上限
|
||||
# thresholdRatio 超过这个比例就触发压缩
|
||||
# compactionRetries 是压缩后仍超标时的额外重试次数
|
||||
- id: compact-basic
|
||||
name: '@deepseek-ai/dsh-compact-basic'
|
||||
config:
|
||||
@@ -94,6 +95,7 @@ Harness 使用 `cordis.yml` 描述一个 Agent 加载哪些插件、以什么参
|
||||
thresholdRatio: 0.8
|
||||
retainTokens: 20480
|
||||
maxTokens: 8192
|
||||
compactionRetries: 1
|
||||
|
||||
# 子代理:把子任务分配给独立的 Agent 去做
|
||||
# subagent 是服务注册,spawn/fork 是两种委派方式:
|
||||
@@ -125,6 +127,16 @@ Harness 使用 `cordis.yml` 描述一个 Agent 加载哪些插件、以什么参
|
||||
provider: fork
|
||||
toolName: subagent_fork
|
||||
|
||||
# 动态工作流:模型编写一段编排脚本,引擎在独立 worker 线程里运行它,
|
||||
# 并通过上面的 spawn 后端把 agent() 调用分发为子代理
|
||||
- id: workflow-workerthread
|
||||
name: '@deepseek-ai/dsh-workflow-workerthread'
|
||||
config:
|
||||
provider: spawn
|
||||
|
||||
- id: tool-workflow
|
||||
name: '@deepseek-ai/dsh-tool-workflow'
|
||||
|
||||
# 任务追踪:模型可以用 todo_write 记录和更新任务清单
|
||||
- id: tool-todo
|
||||
name: '@deepseek-ai/dsh-tool-todo'
|
||||
@@ -156,9 +168,13 @@ Harness 使用 `cordis.yml` 描述一个 Agent 加载哪些插件、以什么参
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `name` | string | 是 | 插件来源(npm 包名或相对路径) |
|
||||
| `id` | string | 否 | 实例标识符,用于日志和调试 |
|
||||
| `id` | string | 否 | 实例标识符,用于日志和调试。省略时由 loader 生成并写回 |
|
||||
| `config` | object | 否 | 传递给插件的配置 |
|
||||
| `disabled` | boolean | 否 | 设为 `true` 临时禁用该插件 |
|
||||
| `group` | boolean | 否 | 标记该条目为嵌套分组(`config` 为子条目列表) |
|
||||
| `inject` | array \| object | 否 | 声明该插件依赖的服务 |
|
||||
| `intercept` | object | 否 | 按服务名拦截并覆盖下游配置 |
|
||||
| `isolate` | object | 否 | 服务隔离:服务名 → `true` 或隔离标签 |
|
||||
|
||||
### 插件来源 (`name`)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user