Merge remote-tracking branch 'origin/master' into xtr/react-loop-simplification
# Conflicts: # .agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.i18n.yaml # docs/architecture.i18n.yaml # docs/architecture.md # docs/architecture.zh.md # docs/cookbook/extension-cookbook.i18n.yaml # docs/cookbook/extension-cookbook.md # docs/cookbook/extension-cookbook.zh.md # docs/core-data-structures/llm-streaming.i18n.yaml # docs/core-data-structures/session.i18n.yaml # docs/defensive-patterns.i18n.yaml # packages/acp/acp/README.i18n.yaml # packages/client/runtime/README.i18n.yaml # packages/client/ui-conversation/README.i18n.yaml # packages/client/ui-goal/README.i18n.yaml # packages/compact/compact-basic/README.i18n.yaml # packages/context/README.i18n.yaml # packages/context/README.md # packages/context/README.zh.md # packages/context/session-reference/README.i18n.yaml # packages/context/session-reference/README.md # packages/context/session-reference/README.zh.md # packages/context/tmux-context/README.i18n.yaml # packages/core/session/README.i18n.yaml # packages/core/session/README.md # packages/core/session/README.zh.md # packages/goal/command-goal/README.i18n.yaml # packages/goal/goal-session/README.i18n.yaml # packages/goal/goal-session/README.zh.md # packages/guard/README.i18n.yaml # packages/guard/README.md # packages/guard/README.zh.md # packages/guard/repeat-tool-guard/README.i18n.yaml # packages/host/apiproxy/README.i18n.yaml # packages/host/apiproxy/README.md # packages/host/apiproxy/README.zh.md # packages/plan/plan-mode/README.i18n.yaml # packages/sdk/sdk-client/README.i18n.yaml # packages/sdk/sdk-client/README.md # packages/sdk/sdk-client/README.zh.md # packages/session-persistence/session-persistence/README.i18n.yaml # packages/subagent/subagent-dsh-sdk/README.i18n.yaml # python/sdk/README.i18n.yaml
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/user/develop/basic/config.md
|
||||
config.md: 26d2d48ebede74194fbf306aa97d214bdb99b722
|
||||
config.zh.md: 1ca727b56b661df315026902d48fbcaf507fe08b
|
||||
config.md: 11a2311464789f74537cc7c4435f83ec07ca26fd
|
||||
config.zh.md: 4e827ecafa6bfaf87c3e3f118425e656e1254787
|
||||
|
||||
@@ -31,13 +31,15 @@ export function apply(ctx: Context, config: Config) {
|
||||
}
|
||||
```
|
||||
|
||||
Configure it in `cordis.yml`:
|
||||
Add the configuration to the inserted local plugin row in `scratch-plugin/cordis.yml`:
|
||||
|
||||
```yaml
|
||||
- name: './src/my-plugin.ts'
|
||||
config:
|
||||
greeting: 'Hi there'
|
||||
maxRetries: 5
|
||||
- insert:
|
||||
- id: hello
|
||||
name: './src/my-plugin.ts'
|
||||
config:
|
||||
greeting: 'Hi there'
|
||||
maxRetries: 5
|
||||
```
|
||||
|
||||
When loading the plugin, Cordis uses the exported schema to validate configuration and fill defaults. Do not export a plain object as `Config`; it does not implement the Standard Schema interface required by Cordis.
|
||||
@@ -91,22 +93,7 @@ The test is whether `cordis.yml` can change the value without a code edit.
|
||||
|
||||
### Fail loudly on invalid configuration
|
||||
|
||||
If configuration refers to an unregistered LLM provider route or another nonexistent resource, fail early instead of silently skipping it:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import type {} from '@deepseek-ai/dsh-llm'
|
||||
|
||||
export interface ModelConfig {
|
||||
provider: string
|
||||
}
|
||||
|
||||
export function apply(ctx: Context, config: ModelConfig) {
|
||||
if (!ctx.llm.listProviders().some(provider => provider.id === config.provider)) {
|
||||
throw new Error(`LLM provider "${config.provider}" is not registered`)
|
||||
}
|
||||
}
|
||||
```
|
||||
Express self-contained constraints in the schema so invalid configuration fails while the plugin loads. References to services or registered resources require dependency injection; the [services tutorial](../framework/service.md) introduces that contract.
|
||||
|
||||
## Work with HMR
|
||||
|
||||
|
||||
@@ -31,13 +31,15 @@ export function apply(ctx: Context, config: Config) {
|
||||
}
|
||||
```
|
||||
|
||||
用户在 `cordis.yml` 中这样使用:
|
||||
在 `scratch-plugin/cordis.yml` 新插入的本地插件行中添加配置:
|
||||
|
||||
```yaml
|
||||
- name: './src/my-plugin.ts'
|
||||
config:
|
||||
greeting: 'Hi there'
|
||||
maxRetries: 5
|
||||
- insert:
|
||||
- id: hello
|
||||
name: './src/my-plugin.ts'
|
||||
config:
|
||||
greeting: 'Hi there'
|
||||
maxRetries: 5
|
||||
```
|
||||
|
||||
插件加载时,Cordis 会通过导出的 schema 校验配置,并填充未提供字段的默认值。不要导出普通对象作为 `Config`,因为它不满足 Cordis 要求的 Standard Schema 接口。
|
||||
@@ -91,22 +93,7 @@ export interface Config {
|
||||
|
||||
### 配置错误要响亮
|
||||
|
||||
如果配置引用了未注册的 LLM 提供方路由或其他不存在的资源,应该尽早报错,而不是静默跳过:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import type {} from '@deepseek-ai/dsh-llm'
|
||||
|
||||
export interface ModelConfig {
|
||||
provider: string
|
||||
}
|
||||
|
||||
export function apply(ctx: Context, config: ModelConfig) {
|
||||
if (!ctx.llm.listProviders().some(provider => provider.id === config.provider)) {
|
||||
throw new Error(`LLM provider "${config.provider}" is not registered`)
|
||||
}
|
||||
}
|
||||
```
|
||||
在 schema 中表达自身完备的约束,使无效配置在插件加载时失败。对服务或已注册资源的引用需要依赖注入;[服务教程](../framework/service.md)会介绍这项契约。
|
||||
|
||||
## 配合 HMR
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/user/develop/basic/index.md
|
||||
index.md: 5a9f8dfb8f2d87dfbd2ba30b4d09d002ae9b635c
|
||||
index.zh.md: 6f8926aeac5dcf667e4d311505ef7b2a321659c2
|
||||
index.md: 45c8dfe495cd99da46a7b259b407af8deff570b3
|
||||
index.zh.md: 9d8ee47e6f07fb4a897f49487cb7327904573c1b
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
|
||||
English | [中文](index.zh.md)
|
||||
|
||||
This guide creates a minimal Harness plugin and loads it into an agent.
|
||||
This tutorial creates a minimal Harness plugin and loads it into the Web UI. Start from a repository checkout that has completed the [quick start](../../guide/quickstart.md).
|
||||
|
||||
## Create a local project
|
||||
|
||||
From the repository root, create a scratch project for the tutorial:
|
||||
|
||||
```sh
|
||||
mkdir -p scratch-plugin/src
|
||||
```
|
||||
|
||||
## What is a plugin?
|
||||
|
||||
@@ -22,7 +30,7 @@ That is the complete shape.
|
||||
|
||||
## Create the plugin file
|
||||
|
||||
Create `src/my-plugin.ts` in your project:
|
||||
Create `scratch-plugin/src/my-plugin.ts`:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
@@ -37,14 +45,21 @@ export function apply(ctx: Context) {
|
||||
|
||||
## Register it in cordis.yml
|
||||
|
||||
Add an entry to `cordis.yml`:
|
||||
Create `scratch-plugin/cordis.yml` as a Web overlay that inserts the local plugin:
|
||||
|
||||
```yaml
|
||||
- id: hello
|
||||
name: './src/my-plugin.ts'
|
||||
- insert:
|
||||
- id: hello
|
||||
name: './src/my-plugin.ts'
|
||||
```
|
||||
|
||||
After startup, the console prints `[hello-plugin] plugin loaded!`.
|
||||
Start the Web UI with that overlay:
|
||||
|
||||
```sh
|
||||
pnpm run dsh web --config ./scratch-plugin/cordis.yml
|
||||
```
|
||||
|
||||
Open `http://127.0.0.1:3080`. The terminal prints `[hello-plugin] plugin loaded!` during startup.
|
||||
|
||||
## Automatic cleanup
|
||||
|
||||
@@ -120,35 +135,6 @@ export default class MyService extends Service {
|
||||
|
||||
Function form is sufficient in most cases. Use class form when the plugin provides a service to other plugins; see [services and dependencies](../framework/service.md).
|
||||
|
||||
## Complete example
|
||||
|
||||
A minimal tool plugin registers its definition on `ctx.tools`:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const name = 'greet-tool'
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'greet',
|
||||
description: 'Greet the named person.',
|
||||
parameters: {
|
||||
name: { type: 'string', required: true },
|
||||
},
|
||||
output: {
|
||||
schema: { type: 'string' },
|
||||
render: (_args, value) => [{ type: 'text', text: value }],
|
||||
},
|
||||
async execute(args) {
|
||||
return `Hello, ${args.name}!`
|
||||
},
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Build a tool](./tool.md) — learn the tool definition DSL
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
|
||||
[English](index.md) | 中文
|
||||
|
||||
本文带你编写一个最小的 Harness 插件并加载到 agent(智能体)中。
|
||||
本教程会创建一个最小的 Harness 插件,并将其加载到 Web UI 中。请从已完成[快速开始](../../guide/quickstart.md)的仓库检出开始。
|
||||
|
||||
## 创建本地项目
|
||||
|
||||
在仓库根目录创建本教程使用的临时项目:
|
||||
|
||||
```sh
|
||||
mkdir -p scratch-plugin/src
|
||||
```
|
||||
|
||||
## 插件是什么
|
||||
|
||||
@@ -22,7 +30,7 @@ export function apply(ctx: Context) {
|
||||
|
||||
## 创建插件文件
|
||||
|
||||
在你的项目目录下创建 `src/my-plugin.ts`:
|
||||
创建 `scratch-plugin/src/my-plugin.ts`:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
@@ -37,14 +45,21 @@ export function apply(ctx: Context) {
|
||||
|
||||
## 注册到 cordis.yml
|
||||
|
||||
在你的 `cordis.yml` 中添加一条:
|
||||
创建 `scratch-plugin/cordis.yml`,作为插入本地插件的 Web 覆盖层:
|
||||
|
||||
```yaml
|
||||
- id: hello
|
||||
name: './src/my-plugin.ts'
|
||||
- insert:
|
||||
- id: hello
|
||||
name: './src/my-plugin.ts'
|
||||
```
|
||||
|
||||
启动后你会在控制台看到 `[hello-plugin] plugin loaded!`。
|
||||
使用该覆盖层启动 Web UI:
|
||||
|
||||
```sh
|
||||
pnpm run dsh web --config ./scratch-plugin/cordis.yml
|
||||
```
|
||||
|
||||
打开 `http://127.0.0.1:3080`。启动期间,终端会打印 `[hello-plugin] plugin loaded!`。
|
||||
|
||||
## 自动清理
|
||||
|
||||
@@ -120,35 +135,6 @@ export default class MyService extends Service {
|
||||
|
||||
大多数情况下,函数形式足够了。当插件需要向其他插件提供服务时,可使用类形式(见 [服务与依赖](../framework/service.md))。
|
||||
|
||||
## 完整示例
|
||||
|
||||
最小的工具插件会在 `ctx.tools` 上注册其定义:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const name = 'greet-tool'
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'greet',
|
||||
description: 'Greet the named person.',
|
||||
parameters: {
|
||||
name: { type: 'string', required: true },
|
||||
},
|
||||
output: {
|
||||
schema: { type: 'string' },
|
||||
render: (_args, value) => [{ type: 'text', text: value }],
|
||||
},
|
||||
async execute(args) {
|
||||
return `Hello, ${args.name}!`
|
||||
},
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- [开发一个工具](./tool.md) — 详细了解工具定义 DSL
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/user/develop/basic/tool.md
|
||||
tool.md: 0d7cbc3f0b86f88fb67aeff6aa61181dff2912ee
|
||||
tool.zh.md: 37362c510a07fcc32e9eee2b578d82907788e471
|
||||
tool.md: 93a1a96feba814a564f8800c8e7b865fe9c0cb73
|
||||
tool.zh.md: 18e6b9b5d9c17b00c26aa7b98e6ac4531315dc5e
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
|
||||
English | [中文](tool.zh.md)
|
||||
|
||||
A tool is a capability the model can call. This guide builds one with `defineTool`.
|
||||
This tutorial adds a `greet` tool to the Web UI. Complete [Your first plugin](./) first and keep its `scratch-plugin` directory.
|
||||
|
||||
## Minimal example
|
||||
## Create the tool plugin
|
||||
|
||||
Replace `scratch-plugin/src/my-plugin.ts` with:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const name = 'my-tool'
|
||||
export const name = 'greet-tool'
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
@@ -25,221 +27,26 @@ export function apply(ctx: Context) {
|
||||
render: (_args, value) => [{ type: 'text', text: value }],
|
||||
},
|
||||
async execute(args) {
|
||||
// args is inferred as { name: string }.
|
||||
return `Hello, ${args.name}!`
|
||||
},
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
## Parameter definitions
|
||||
`inject` makes Cordis wait for the tool registry. `defineTool` infers and validates `args` from `parameters`; `execute` returns the canonical value declared by `output.schema`, and `output.render` converts that value to model-facing content.
|
||||
|
||||
`parameters` uses a compact format that the framework converts to the JSON Schema sent to the model.
|
||||
## Run and call the tool
|
||||
|
||||
### Primitive types
|
||||
Restart the development command if it is not running:
|
||||
|
||||
```ts
|
||||
export const parameters = {
|
||||
path: { type: 'string', required: true },
|
||||
limit: { type: 'integer' },
|
||||
recursive: { type: 'boolean' },
|
||||
parent: { type: 'null' },
|
||||
}
|
||||
// Inferred type: { path: string; limit?: number; recursive?: boolean; parent?: null }
|
||||
```sh
|
||||
pnpm run dsh web --config ./scratch-plugin/cordis.yml
|
||||
```
|
||||
|
||||
### Enums
|
||||
|
||||
```ts
|
||||
export const parameters = {
|
||||
mode: { type: 'string', required: true, enum: ['read', 'write', 'append'] },
|
||||
}
|
||||
// Inferred type: { mode: 'read' | 'write' | 'append' }
|
||||
```
|
||||
|
||||
### Nested objects
|
||||
|
||||
```ts
|
||||
export const parameters = {
|
||||
options: {
|
||||
type: 'object',
|
||||
additionalProperties: true,
|
||||
properties: {
|
||||
timeout: { type: 'number' },
|
||||
retries: { type: 'number' },
|
||||
},
|
||||
},
|
||||
}
|
||||
// The declared fields are inferred; additional JSON-valued keys are allowed.
|
||||
```
|
||||
|
||||
### Arrays
|
||||
|
||||
```ts
|
||||
export const parameters = {
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
}
|
||||
// Inferred type: { tags?: string[] }
|
||||
```
|
||||
|
||||
### Property fields
|
||||
|
||||
| Field | Type | Meaning |
|
||||
|------|------|------|
|
||||
| `type` | `'string' \| 'number' \| 'integer' \| 'boolean' \| 'null' \| 'object' \| 'array' \| 'json'` | Value type; `json` accepts any lossless JSON value |
|
||||
| `required` | `true` | Marks the property required and affects inference |
|
||||
| `description` | `string` | Description sent to the model |
|
||||
| `enum` / `const` | matching scalar values | Allowed literal values, checked at author and runtime boundaries |
|
||||
| `properties` | `ParameterSchemaSpec` | Nested properties for an object |
|
||||
| `additionalProperties` | `true \| false` | Required on every explicit object node |
|
||||
| `items` | `ValueSchemaSpec` | Element schema for an array |
|
||||
| `oneOf` | at least two `ValueSchemaSpec` branches | Requires exactly one matching branch; used instead of `type` |
|
||||
|
||||
The outer `parameters` map is an implicit open object. Explicit nested objects choose their openness; raw JSON Schema registered without `defineTool` keeps JSON Schema's open-by-default behavior.
|
||||
|
||||
## The execute function
|
||||
|
||||
`execute` receives validated, inferred `args` and an `exec` execution context:
|
||||
|
||||
```ts
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'example',
|
||||
description: 'Return an example result.',
|
||||
parameters: {},
|
||||
output: {
|
||||
schema: { type: 'string' },
|
||||
render: (_args, value) => [{ type: 'text', text: value }],
|
||||
},
|
||||
async execute(args, exec) {
|
||||
// args: inferred from parameters
|
||||
// exec: ToolExecution context
|
||||
|
||||
// Return the value declared by output.schema.
|
||||
void args
|
||||
void exec
|
||||
return 'result here'
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Return value
|
||||
|
||||
`execute` returns the lossless JSON value declared by `output.schema`. `output.render(args, value)` separately turns that validated value into the Native/model-facing content:
|
||||
|
||||
```ts ignore-check
|
||||
output: {
|
||||
schema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
path: { type: 'string', required: true },
|
||||
content: { type: 'string', required: true },
|
||||
},
|
||||
},
|
||||
render: (_args, value) => [{ type: 'text', text: value.content }],
|
||||
},
|
||||
async execute(args) {
|
||||
return { path: args.path, content: await readFile(args.path, 'utf8') }
|
||||
}
|
||||
```
|
||||
|
||||
The canonical value is available to execution-time programmatic callers and is not persisted in `tool/result`; the rendered content and optional `presentationMeta` are the replayable projections. A body value that does not satisfy the schema, or is not lossless JSON, becomes an `INVALID_TOOL_OUTPUT` failure.
|
||||
|
||||
### Argument validation
|
||||
|
||||
Before calling `execute`, `defineTool` validates model-generated arguments. Invalid input raises `ToolArgsError`; the framework turns it into an `isError` result so the model can correct its call.
|
||||
|
||||
Do not repeat type validation inside `execute`.
|
||||
|
||||
## Presentation
|
||||
|
||||
A tool can define transport-neutral presentation methods for terminal and web clients:
|
||||
|
||||
```ts ignore-check
|
||||
defineTool({
|
||||
name: 'bash',
|
||||
// ...
|
||||
output: {
|
||||
schema: { type: 'string' },
|
||||
render: (_args, value) => [{ type: 'text', text: value }],
|
||||
},
|
||||
presentCall(args) {
|
||||
return {
|
||||
card: 'terminal',
|
||||
title: args.command,
|
||||
}
|
||||
},
|
||||
presentResult(args, result) {
|
||||
return {
|
||||
card: 'terminal',
|
||||
output: result.content.map(b => b.type === 'text' ? b.text : '').join(''),
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
`presentCall` and `presentResult` are **pure functions**. Streaming UI and session replay may call them more than once.
|
||||
|
||||
## Registration and unloading
|
||||
|
||||
`ctx.tools.register()` returns a disposer, but a registration made through `ctx` is already tracked by the framework. Unloading the plugin removes the tool automatically, so the plugin does not call the disposer itself.
|
||||
|
||||
```ts ignore-check
|
||||
// This is sufficient:
|
||||
ctx.tools.register(defineTool({ /* ... */ }))
|
||||
|
||||
// No saved disposer or extra cleanup registration is needed.
|
||||
```
|
||||
|
||||
## Complete example
|
||||
|
||||
This tool counts files in a directory:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import { readdir } from 'node:fs/promises'
|
||||
|
||||
export const name = 'file-counter'
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'count_files',
|
||||
description: 'Count files in a directory.',
|
||||
parameters: {
|
||||
path: { type: 'string', required: true, description: 'Directory path' },
|
||||
extension: { type: 'string', description: 'Filter by extension (e.g. ".ts")' },
|
||||
},
|
||||
output: {
|
||||
schema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
count: { type: 'integer', required: true },
|
||||
files: { type: 'array', required: true, items: { type: 'string' } },
|
||||
},
|
||||
},
|
||||
render: (_args, value) => [{ type: 'text', text: `Found ${value.count} files.` }],
|
||||
},
|
||||
async execute(args) {
|
||||
const entries = await readdir(args.path, { withFileTypes: true })
|
||||
let files = entries.filter(e => e.isFile())
|
||||
if (args.extension) {
|
||||
files = files.filter(f => f.name.endsWith(args.extension!))
|
||||
}
|
||||
return { count: files.length, files: files.map(file => file.name) }
|
||||
},
|
||||
}))
|
||||
}
|
||||
```
|
||||
Open `http://127.0.0.1:3080` and ask: `Use the greet tool to greet Ada.` The model can call `greet` and receives `Hello, Ada!` as the tool result.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Plugin configuration](./config.md) — make the tool configurable
|
||||
- [Capability layering](../practice/) — understand the interface/implementation/consumer pattern
|
||||
- [Plugin configuration](./config.md) — make the greeting configurable.
|
||||
- [Tool authoring reference](../../../cookbook/adding-a-tool.md) — look up nested schemas, canonical values, background work, policy hooks, Code Mode, and UI cards.
|
||||
- [Capability layering](../practice/) — split a replaceable capability into interface, implementation, and consumer packages.
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
# 开发一个工具
|
||||
# 构建工具
|
||||
|
||||
[English](tool.md) | 中文
|
||||
|
||||
工具是模型可以调用的能力。本文介绍如何用 `defineTool` 编写一个工具。
|
||||
本教程会在 Web UI 中添加一个 `greet` 工具。请先完成[第一个插件](./),并保留其中的 `scratch-plugin` 目录。
|
||||
|
||||
## 最小示例
|
||||
## 创建工具插件
|
||||
|
||||
将 `scratch-plugin/src/my-plugin.ts` 替换为:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const name = 'my-tool'
|
||||
export const name = 'greet-tool'
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
@@ -25,221 +27,26 @@ export function apply(ctx: Context) {
|
||||
render: (_args, value) => [{ type: 'text', text: value }],
|
||||
},
|
||||
async execute(args) {
|
||||
// args is inferred as { name: string }.
|
||||
return `Hello, ${args.name}!`
|
||||
},
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
## 参数定义
|
||||
`inject` 让 Cordis 等待工具注册表就绪。`defineTool` 根据 `parameters` 推导并校验 `args`;`execute` 返回 `output.schema` 声明的规范值,`output.render` 再将该值转换为面向模型的内容。
|
||||
|
||||
`parameters` 用一种简洁的格式描述参数,框架会自动转换为模型需要的 JSON Schema。
|
||||
## 运行并调用工具
|
||||
|
||||
### 基本类型
|
||||
如果开发命令未在运行,请重新启动:
|
||||
|
||||
```ts
|
||||
export const parameters = {
|
||||
path: { type: 'string', required: true },
|
||||
limit: { type: 'integer' },
|
||||
recursive: { type: 'boolean' },
|
||||
parent: { type: 'null' },
|
||||
}
|
||||
// Inferred type: { path: string; limit?: number; recursive?: boolean; parent?: null }
|
||||
```sh
|
||||
pnpm run dsh web --config ./scratch-plugin/cordis.yml
|
||||
```
|
||||
|
||||
### 枚举
|
||||
|
||||
```ts
|
||||
export const parameters = {
|
||||
mode: { type: 'string', required: true, enum: ['read', 'write', 'append'] },
|
||||
}
|
||||
// Inferred type: { mode: 'read' | 'write' | 'append' }
|
||||
```
|
||||
|
||||
### 嵌套对象
|
||||
|
||||
```ts
|
||||
export const parameters = {
|
||||
options: {
|
||||
type: 'object',
|
||||
additionalProperties: true,
|
||||
properties: {
|
||||
timeout: { type: 'number' },
|
||||
retries: { type: 'number' },
|
||||
},
|
||||
},
|
||||
}
|
||||
// The declared fields are inferred; additional JSON-valued keys are allowed.
|
||||
```
|
||||
|
||||
### 数组
|
||||
|
||||
```ts
|
||||
export const parameters = {
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
}
|
||||
// Inferred type: { tags?: string[] }
|
||||
```
|
||||
|
||||
### 每个属性的字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `type` | `'string' \| 'number' \| 'integer' \| 'boolean' \| 'null' \| 'object' \| 'array' \| 'json'` | 值类型;`json` 接受任意无损 JSON 值 |
|
||||
| `required` | `true` | 标记为必填(影响类型推导) |
|
||||
| `description` | `string` | 发送给模型的描述 |
|
||||
| `enum` / `const` | 匹配类型的标量值 | 允许的字面量值,在编写和运行时边界校验 |
|
||||
| `properties` | `ParameterSchemaSpec` | 对象的嵌套属性 |
|
||||
| `additionalProperties` | `true \| false` | 每个显式对象节点都必须声明 |
|
||||
| `items` | `ValueSchemaSpec` | 数组的元素 schema |
|
||||
| `oneOf` | 至少两个 `ValueSchemaSpec` 分支 | 要求恰好匹配一个分支;代替 `type` 使用 |
|
||||
|
||||
外层 `parameters` 映射是一个隐式的开放对象。显式嵌套对象需自行选择是否开放;不通过 `defineTool` 注册的原始 JSON Schema 保持 JSON Schema 的默认开放语义。
|
||||
|
||||
## execute 函数
|
||||
|
||||
`execute` 接收经过校验的 `args`(类型自动推导)和一个 `exec` 上下文对象:
|
||||
|
||||
```ts
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'example',
|
||||
description: 'Return an example result.',
|
||||
parameters: {},
|
||||
output: {
|
||||
schema: { type: 'string' },
|
||||
render: (_args, value) => [{ type: 'text', text: value }],
|
||||
},
|
||||
async execute(args, exec) {
|
||||
// args: inferred from parameters
|
||||
// exec: ToolExecution context
|
||||
|
||||
// Return the value declared by output.schema.
|
||||
void args
|
||||
void exec
|
||||
return 'result here'
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### 返回值
|
||||
|
||||
`execute` 返回由 `output.schema` 声明的无损 JSON 值。`output.render(args, value)` 会将经过校验的值另外转换为 Native/模型可见的内容:
|
||||
|
||||
```ts ignore-check
|
||||
output: {
|
||||
schema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
path: { type: 'string', required: true },
|
||||
content: { type: 'string', required: true },
|
||||
},
|
||||
},
|
||||
render: (_args, value) => [{ type: 'text', text: value.content }],
|
||||
},
|
||||
async execute(args) {
|
||||
return { path: args.path, content: await readFile(args.path, 'utf8') }
|
||||
}
|
||||
```
|
||||
|
||||
执行期间的程序化调用方可以使用规范值,但 `tool/result` 不会持久化该值;渲染后的内容和可选的 `presentationMeta` 才是可回放的投影。工具主体返回的值若不满足 schema 或不是无损 JSON,就会变为 `INVALID_TOOL_OUTPUT` 失败。
|
||||
|
||||
### 参数校验
|
||||
|
||||
`defineTool` 在调用 `execute` 之前会自动校验模型生成的参数。如果参数不合法,会抛出 `ToolArgsError`,框架将其转换为 `isError` 结果返回给模型,让模型自行修正。
|
||||
|
||||
你不需要在 `execute` 里手动校验参数类型。
|
||||
|
||||
## 展示层(Presentation)
|
||||
|
||||
工具可以定义与传输方式无关的展示方法,供终端和 Web 客户端使用:
|
||||
|
||||
```ts ignore-check
|
||||
defineTool({
|
||||
name: 'bash',
|
||||
// ...
|
||||
output: {
|
||||
schema: { type: 'string' },
|
||||
render: (_args, value) => [{ type: 'text', text: value }],
|
||||
},
|
||||
presentCall(args) {
|
||||
return {
|
||||
card: 'terminal',
|
||||
title: args.command,
|
||||
}
|
||||
},
|
||||
presentResult(args, result) {
|
||||
return {
|
||||
card: 'terminal',
|
||||
output: result.content.map(b => b.type === 'text' ? b.text : '').join(''),
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
`presentCall` 和 `presentResult` 是**纯函数**,不能有副作用——UI 可能在流式传输中和会话回放中多次调用它们。
|
||||
|
||||
## 注册与卸载
|
||||
|
||||
`ctx.tools.register()` 返回值就是 disposer。但由于你在 `ctx` 上调用,框架已经自动追踪了这个注册——插件卸载时会自动移除工具。你不需要手动调用 disposer。
|
||||
|
||||
```ts ignore-check
|
||||
// This is sufficient:
|
||||
ctx.tools.register(defineTool({ /* ... */ }))
|
||||
|
||||
// No saved disposer or extra cleanup registration is needed.
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
一个文件计数工具:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import { readdir } from 'node:fs/promises'
|
||||
|
||||
export const name = 'file-counter'
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'count_files',
|
||||
description: 'Count files in a directory.',
|
||||
parameters: {
|
||||
path: { type: 'string', required: true, description: 'Directory path' },
|
||||
extension: { type: 'string', description: 'Filter by extension (e.g. ".ts")' },
|
||||
},
|
||||
output: {
|
||||
schema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
count: { type: 'integer', required: true },
|
||||
files: { type: 'array', required: true, items: { type: 'string' } },
|
||||
},
|
||||
},
|
||||
render: (_args, value) => [{ type: 'text', text: `Found ${value.count} files.` }],
|
||||
},
|
||||
async execute(args) {
|
||||
const entries = await readdir(args.path, { withFileTypes: true })
|
||||
let files = entries.filter(e => e.isFile())
|
||||
if (args.extension) {
|
||||
files = files.filter(f => f.name.endsWith(args.extension!))
|
||||
}
|
||||
return { count: files.length, files: files.map(file => file.name) }
|
||||
},
|
||||
}))
|
||||
}
|
||||
```
|
||||
打开 `http://127.0.0.1:3080`,然后输入:`Use the greet tool to greet Ada.` 模型可以调用 `greet`,并收到 `Hello, Ada!` 这一工具结果。
|
||||
|
||||
## 下一步
|
||||
|
||||
- [插件配置](./config.md) — 让你的工具可配置
|
||||
- [能力分层](../practice/) — 了解接口/实现/消费方模式
|
||||
- [插件配置](./config.md) — 让问候语可配置。
|
||||
- [工具编写参考](../../../cookbook/adding-a-tool.md) — 查阅嵌套 schema、规范值、后台工作、策略钩子、Code Mode 和 UI 卡片。
|
||||
- [能力分层](../practice/) — 将可替换能力拆分为接口、实现和消费方包。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/user/develop/practice/index.md
|
||||
index.md: e197d499d7f5bd9911ea60bebf584251cd4ed915
|
||||
index.zh.md: cacf13e8b23060ea5c309d30e51d732f09c76000
|
||||
index.md: c3306725f47993aa9d3322423261a260754c1d5f
|
||||
index.zh.md: 3609cbce21e17ce9ca0a40b69999b293dee29012
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
English | [中文](index.zh.md)
|
||||
|
||||
This page has two parts: a concept reference for the three-layer capability pattern, followed by an advanced tutorial that builds one capability. Complete the [basic plugin path](../basic/) and [services tutorial](../framework/service.md) first.
|
||||
|
||||
## Concept reference
|
||||
|
||||
When a capability is general enough to need replaceable implementations, such as Bash execution, Harness splits it into three packages: an **interface**, an **implementation**, and a **consumer**. Each layer can evolve or be replaced independently.
|
||||
|
||||
## Bash example
|
||||
@@ -32,10 +36,7 @@ One interface can have multiple implementations selected through `cordis.yml`:
|
||||
# Local execution
|
||||
- name: '@deepseek-ai/dsh-bash-local'
|
||||
|
||||
# Or a future remote sandbox implementation
|
||||
# - name: '@deepseek-ai/dsh-bash-remote'
|
||||
# config:
|
||||
# endpoint: 'https://sandbox.example.com'
|
||||
# Replace this row with another package that implements the same service.
|
||||
```
|
||||
|
||||
The interface and tool remain unchanged while the implementation changes.
|
||||
@@ -52,17 +53,9 @@ The interface and tool remain unchanged while the implementation changes.
|
||||
- The consumer depends on the interface.
|
||||
- The implementation and consumer **do not depend on each other**.
|
||||
|
||||
## Built-in three-layer capabilities
|
||||
The [capability-seam reference](../../../capability-seams.md) owns the current built-in families and package links.
|
||||
|
||||
| Capability | Interface | Implementation | Consumer |
|
||||
|------|-------------|------|---------------|
|
||||
| Bash | `dsh-bash` | `dsh-bash-local` | `dsh-tool-bash` |
|
||||
| Filesystem | `dsh-fs` | `dsh-fs-local` + `dsh-fs-policy` | `dsh-tool-fs` |
|
||||
| Web | `dsh-web` | `dsh-web-fetch-local` / `dsh-web-search-*` | `dsh-tool-web` |
|
||||
| Subagent | `dsh-subagent` | `dsh-subagent-spawn` / `dsh-subagent-fork` | `dsh-tool-subagent` |
|
||||
| Compaction | `dsh-compact` | `dsh-compact-basic` | The implementation consumes agent-loop extension events |
|
||||
|
||||
## Develop a three-layer capability
|
||||
## Tutorial: develop a three-layer capability
|
||||
|
||||
### Step 1: define the interface
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
[English](index.md) | 中文
|
||||
|
||||
本文分为两部分:先参考三层能力模式的概念,再通过高级教程构建一项能力。请先完成[基础插件路径](../basic/)和[服务教程](../framework/service.md)。
|
||||
|
||||
## 概念参考
|
||||
|
||||
当一项能力足够通用,需要支持可替换的实现时(例如 Bash 执行),Harness 会将其拆成三个包:**接口**、**实现**和**消费方**。这样便可独立替换其中任何一层。
|
||||
|
||||
## 以 Bash 为例
|
||||
@@ -32,10 +36,7 @@
|
||||
# Local execution
|
||||
- name: '@deepseek-ai/dsh-bash-local'
|
||||
|
||||
# Or a future remote sandbox implementation
|
||||
# - name: '@deepseek-ai/dsh-bash-remote'
|
||||
# config:
|
||||
# endpoint: 'https://sandbox.example.com'
|
||||
# Replace this row with another package that implements the same service.
|
||||
```
|
||||
|
||||
更换实现时,接口和工具均保持不变。
|
||||
@@ -52,17 +53,9 @@
|
||||
- 消费方依赖接口。
|
||||
- 实现和消费方**互不依赖**。
|
||||
|
||||
## Harness 中内置的三件套
|
||||
当前内置系列及其包链接由[能力 seam 参考](../../../capability-seams.md)负责。
|
||||
|
||||
| 能力 | 接口(seam) | 实现 | 消费方(工具) |
|
||||
|------|-------------|------|---------------|
|
||||
| Bash | `dsh-bash` | `dsh-bash-local` | `dsh-tool-bash` |
|
||||
| 文件系统 | `dsh-fs` | `dsh-fs-local` + `dsh-fs-policy` | `dsh-tool-fs` |
|
||||
| Web | `dsh-web` | `dsh-web-fetch-local` / `dsh-web-search-*` | `dsh-tool-web` |
|
||||
| 子代理 | `dsh-subagent` | `dsh-subagent-spawn` / `dsh-subagent-fork` | `dsh-tool-subagent` |
|
||||
| 压缩 | `dsh-compact` | `dsh-compact-basic` | 由实现插件消费 agent-loop 的扩展事件 |
|
||||
|
||||
## 开发你自己的三件套
|
||||
## 教程:开发三层能力
|
||||
|
||||
### 第一步:定义接口
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/user/guide/config.md
|
||||
config.md: 4e438cc3a400de71934d047108024ff5be4ef7d2
|
||||
config.zh.md: e0b0285b110a808b1284209a84f78e114634df52
|
||||
config.md: ddf4df264e5534fc3b74991941c2f3f82376d53f
|
||||
config.zh.md: 56ac0146ddae83dbfc86f479030efdb5772a3aaf
|
||||
|
||||
@@ -47,7 +47,7 @@ A minimal configuration is a list of plugin entries:
|
||||
toolName: my_tool
|
||||
```
|
||||
|
||||
Plugins load in file order. Place plugins that depend on services after the applications or capability plugins that provide them. Missing models, tools, and plugins fail as early as possible instead of being silently ignored.
|
||||
Cordis starts sibling entries concurrently. A plugin declares required services through `inject`; Cordis waits for those services before applying the plugin, so file order does not establish dependency readiness. Missing models, tools, and plugins fail as early as possible instead of being silently ignored.
|
||||
|
||||
## CLI overlays
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](config.md) | 中文
|
||||
|
||||
Harness 使用 `cordis.yml` 描述 Agent 加载哪些插件以及每个插件的参数。配置文件负责组合能力;每个包真正支持的字段和默认值由源码生成的配置目录负责记录,避免两份手写表格逐渐不一致。
|
||||
Harness 使用 `cordis.yml` 描述 agent(智能体)加载哪些插件以及每个插件的参数。配置文件负责组合能力;每个包真正支持的字段和默认值由源码生成的配置目录负责记录,避免两份手写表格逐渐不一致。
|
||||
|
||||
## 从真实配置开始
|
||||
|
||||
@@ -47,7 +47,7 @@ Harness 使用 `cordis.yml` 描述 Agent 加载哪些插件以及每个插件的
|
||||
toolName: my_tool
|
||||
```
|
||||
|
||||
插件按文件中的顺序加载。依赖其他服务的插件应该排在提供这些服务的应用或能力插件之后;引用不存在的模型、工具或插件会尽早报错,而不是被静默忽略。
|
||||
Cordis 会并发启动同级配置项。插件通过 `inject` 声明必需服务;Cordis 会等到这些服务就绪后再应用该插件,因此文件顺序不能保证依赖已就绪。引用不存在的模型、工具或插件会尽早报错,而不是被静默忽略。
|
||||
|
||||
## CLI 覆盖层
|
||||
|
||||
|
||||
Reference in New Issue
Block a user