fix(docs): align site with bilingual source pairs
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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
|
||||
events.md: 0c57681a55ea0200fe8f33293176fc94f09a4ce5
|
||||
events.zh.md: 3e14739d4a97ba014d545c9f226000507aaeacef
|
||||
@@ -0,0 +1,143 @@
|
||||
# Event system
|
||||
|
||||
English | [中文](events.zh.md)
|
||||
|
||||
Events are the core communication mechanism between Cordis plugins. Harness uses them extensively for loosely coupled extension points.
|
||||
|
||||
## Basic use
|
||||
|
||||
### Listen for an event
|
||||
|
||||
```ts ignore-check
|
||||
ctx.on('event-name', (payload) => {
|
||||
// Handle the event.
|
||||
})
|
||||
```
|
||||
|
||||
### Emit an event
|
||||
|
||||
```ts ignore-check
|
||||
ctx.emit('event-name', payload)
|
||||
```
|
||||
|
||||
## Event modes
|
||||
|
||||
Cordis provides several event modes for different interaction contracts.
|
||||
|
||||
### emit — broadcast
|
||||
|
||||
Every listener runs synchronously and return values are ignored:
|
||||
|
||||
```ts ignore-check
|
||||
// Emit
|
||||
ctx.emit('my-plugin/ready', { id: 'worker-1' })
|
||||
|
||||
// Listen
|
||||
ctx.on('my-plugin/ready', ({ id }) => {
|
||||
console.log(`${id} is ready`)
|
||||
})
|
||||
```
|
||||
|
||||
### bail — short circuit
|
||||
|
||||
Listeners run in order; the first non-`undefined` result becomes the final result:
|
||||
|
||||
```ts ignore-check
|
||||
// Dispatch
|
||||
const result = ctx.bail('some-check', input)
|
||||
|
||||
// Listen: a returned value stops later listeners.
|
||||
ctx.on('some-check', (input) => {
|
||||
if (shouldBlock(input)) return 'blocked'
|
||||
// Return undefined to continue to the next listener.
|
||||
})
|
||||
```
|
||||
|
||||
### serial — ordered execution
|
||||
|
||||
Listeners run in registration order and asynchronous results are awaited. The first listener to return a non-empty value stops further execution:
|
||||
|
||||
```ts ignore-check
|
||||
await ctx.serial('setup-phase', context)
|
||||
```
|
||||
|
||||
### waterfall — pipeline
|
||||
|
||||
Each listener may wrap the downstream result to form a processing chain. A listener **must call `next()` to delegate downstream**; omitting the call vetoes the pipeline:
|
||||
|
||||
```ts ignore-check
|
||||
// Dispatch
|
||||
const output = await ctx.waterfall('my-plugin/transform', input, async () => input)
|
||||
|
||||
// Listen: next() is mandatory.
|
||||
ctx.on('my-plugin/transform', async (_input, next) => {
|
||||
const downstream = await next()
|
||||
return downstream.trim()
|
||||
})
|
||||
```
|
||||
|
||||
::: warning
|
||||
A waterfall listener **must call `next()`**. Omitting it vetoes the pipeline by design, enabling interception and gateway behavior.
|
||||
:::
|
||||
|
||||
## Typed events
|
||||
|
||||
Harness uses TypeScript declaration merging for type-safe events:
|
||||
|
||||
```ts
|
||||
import 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Events {
|
||||
'my-plugin/ready': (payload: { id: string }) => void
|
||||
'my-plugin/check': (input: string) => boolean | undefined
|
||||
'my-plugin/transform': (input: string, next: () => Promise<string>) => Promise<string>
|
||||
}
|
||||
}
|
||||
|
||||
// ctx.on('my-plugin/ready', ...) and ctx.emit('my-plugin/ready', ...)
|
||||
// are now inferred correctly.
|
||||
```
|
||||
|
||||
## Cordis events and session records
|
||||
|
||||
Harness Cordis events use `namespace/action` names, including `agent/pre-step`, `agent/request`, `agent/step-result`, `tools/result`, and `session/event`. The generated [event catalog](../../../cordis-catalog/events.md) records complete signatures and modes.
|
||||
|
||||
`turn/*`, `step/*`, `tool/call`, `tool/result`, and `compact/*` are durable session-event types, not same-named Cordis events. To observe them, listen to `session/event` and inspect `event.type`.
|
||||
|
||||
## Event listeners are effects
|
||||
|
||||
A listener registered with `ctx.on()` is removed automatically when its plugin unloads:
|
||||
|
||||
```ts ignore-check
|
||||
export function apply(ctx: Context) {
|
||||
// This listener is removed when the plugin disposes.
|
||||
ctx.on('tools/result', handler)
|
||||
}
|
||||
```
|
||||
|
||||
## Example: logging plugin
|
||||
|
||||
This plugin logs tool calls and results:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const name = 'tool-logger'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.on('tools/result', (exec, result) => {
|
||||
console.log(`[tool] ${exec.name}(${JSON.stringify(exec.arguments)})`)
|
||||
const text = result.content
|
||||
.map(block => block.type === 'text' ? block.text : '')
|
||||
.join('')
|
||||
console.log(`[tool result] ${text.slice(0, 100)}`)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Capability layering](../practice/) — understand events within capability interfaces
|
||||
- [LLM adapters](../practice/llm-adapter.md) — implement a complete LLM backend
|
||||
@@ -0,0 +1,143 @@
|
||||
# 事件系统
|
||||
|
||||
[English](events.md) | 中文
|
||||
|
||||
事件是 Cordis 插件间通信的核心机制。Harness 大量使用事件来实现松耦合的扩展点。
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 监听事件
|
||||
|
||||
```ts ignore-check
|
||||
ctx.on('event-name', (payload) => {
|
||||
// Handle the event.
|
||||
})
|
||||
```
|
||||
|
||||
### 触发事件
|
||||
|
||||
```ts ignore-check
|
||||
ctx.emit('event-name', payload)
|
||||
```
|
||||
|
||||
## 事件模式
|
||||
|
||||
Cordis 提供多种事件触发模式,适用于不同场景:
|
||||
|
||||
### emit — 广播
|
||||
|
||||
所有监听器同步执行,不关心返回值:
|
||||
|
||||
```ts ignore-check
|
||||
// Emit
|
||||
ctx.emit('my-plugin/ready', { id: 'worker-1' })
|
||||
|
||||
// Listen
|
||||
ctx.on('my-plugin/ready', ({ id }) => {
|
||||
console.log(`${id} is ready`)
|
||||
})
|
||||
```
|
||||
|
||||
### bail — 短路
|
||||
|
||||
依次调用监听器,第一个返回非 `undefined` 值的结果作为最终值:
|
||||
|
||||
```ts ignore-check
|
||||
// Dispatch
|
||||
const result = ctx.bail('some-check', input)
|
||||
|
||||
// Listen: a returned value stops later listeners.
|
||||
ctx.on('some-check', (input) => {
|
||||
if (shouldBlock(input)) return 'blocked'
|
||||
// Return undefined to continue to the next listener.
|
||||
})
|
||||
```
|
||||
|
||||
### serial — 顺序执行
|
||||
|
||||
监听器按注册顺序依次执行,并等待异步结果;第一个返回非空值的监听器会终止后续执行:
|
||||
|
||||
```ts ignore-check
|
||||
await ctx.serial('setup-phase', context)
|
||||
```
|
||||
|
||||
### waterfall — 管道
|
||||
|
||||
每个监听器可以包装下游返回值,形成处理链。**必须调用 `next()` 传递给下游**,不调用即为否决:
|
||||
|
||||
```ts ignore-check
|
||||
// Dispatch
|
||||
const output = await ctx.waterfall('my-plugin/transform', input, async () => input)
|
||||
|
||||
// Listen: next() is mandatory.
|
||||
ctx.on('my-plugin/transform', async (_input, next) => {
|
||||
const downstream = await next()
|
||||
return downstream.trim()
|
||||
})
|
||||
```
|
||||
|
||||
::: warning
|
||||
Waterfall 监听器**必须调用 `next()`**。不调用 `next` 等于否决整个管道,这是故意为之的设计——用于实现拦截/网关逻辑。
|
||||
:::
|
||||
|
||||
## Typed Events
|
||||
|
||||
Harness 使用 TypeScript 声明合并来为事件提供类型安全:
|
||||
|
||||
```ts
|
||||
import 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Events {
|
||||
'my-plugin/ready': (payload: { id: string }) => void
|
||||
'my-plugin/check': (input: string) => boolean | undefined
|
||||
'my-plugin/transform': (input: string, next: () => Promise<string>) => Promise<string>
|
||||
}
|
||||
}
|
||||
|
||||
// ctx.on('my-plugin/ready', ...) and ctx.emit('my-plugin/ready', ...)
|
||||
// are now inferred correctly.
|
||||
```
|
||||
|
||||
## Cordis 事件与会话记录
|
||||
|
||||
Harness 的 Cordis 事件遵循 `namespace/action` 命名,例如 `agent/pre-step`、`agent/request`、`agent/step-result`、`tools/result` 和 `session/event`。完整签名与触发模式见[Events 目录](../../../cordis-catalog/events.md)。
|
||||
|
||||
`turn/*`、`step/*`、`tool/call`、`tool/result` 和 `compact/*` 是持久化的会话事件类型,不是同名 Cordis 事件。需要观察它们时,监听 `session/event` 并检查 `event.type`。
|
||||
|
||||
## 事件也是效果
|
||||
|
||||
通过 `ctx.on()` 注册的监听器会在插件卸载时自动移除:
|
||||
|
||||
```ts ignore-check
|
||||
export function apply(ctx: Context) {
|
||||
// This listener is removed when the plugin disposes.
|
||||
ctx.on('tools/result', handler)
|
||||
}
|
||||
```
|
||||
|
||||
## 实战示例:日志插件
|
||||
|
||||
一个记录所有 tool 调用的简单插件:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const name = 'tool-logger'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.on('tools/result', (exec, result) => {
|
||||
console.log(`[tool] ${exec.name}(${JSON.stringify(exec.arguments)})`)
|
||||
const text = result.content
|
||||
.map(block => block.type === 'text' ? block.text : '')
|
||||
.join('')
|
||||
console.log(`[tool result] ${text.slice(0, 100)}`)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- [能力三件套](../practice/) — 事件在 capability seam 中的角色
|
||||
- [LLM 适配器](../practice/llm-adapter.md) — 实现一个完整的 LLM 后端
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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
|
||||
index.md: bb08e7cb3f9d3a094100806451fd33d1482003cb
|
||||
index.zh.md: 4b9d6d22c8d82bece67c71032a0028b21939f98f
|
||||
@@ -0,0 +1,131 @@
|
||||
# Plugins and lifecycle
|
||||
|
||||
English | [中文](index.zh.md)
|
||||
|
||||
This page describes the Cordis plugin model and lifecycle state machine.
|
||||
|
||||
## Fiber state machine
|
||||
|
||||
Every loaded plugin owns a **Fiber** scope with the following states:
|
||||
|
||||
```
|
||||
PENDING → LOADING → ACTIVE
|
||||
↘ FAILED
|
||||
ACTIVE → UNLOADING → DISPOSED
|
||||
```
|
||||
|
||||
| State | Meaning |
|
||||
|------|------|
|
||||
| PENDING | Declared, but required dependencies are not ready |
|
||||
| LOADING | Dependencies are ready and `apply` is running |
|
||||
| ACTIVE | The plugin is running |
|
||||
| FAILED | `apply` threw an error |
|
||||
| UNLOADING | The plugin is unloading and disposing resources |
|
||||
| DISPOSED | The plugin is fully unloaded |
|
||||
|
||||
## Dependency-driven loading
|
||||
|
||||
A plugin with `inject` waits for every required service before loading:
|
||||
|
||||
```ts ignore-check
|
||||
export const inject = ['tools', 'llm']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// ctx.tools and ctx.llm are ready here.
|
||||
}
|
||||
```
|
||||
|
||||
If a required service disappears, for example during provider replacement, the plugin unloads automatically (ACTIVE → DISPOSED) and loads again when the service returns.
|
||||
|
||||
## Automatic cleanup
|
||||
|
||||
Every registration made through `ctx` is undone when the plugin unloads:
|
||||
|
||||
```ts ignore-check
|
||||
export function apply(ctx: Context) {
|
||||
// Event listener: removed automatically on unload.
|
||||
ctx.on('some-event', handler)
|
||||
|
||||
// Custom resource: the returned disposer runs on unload.
|
||||
ctx.effect(() => {
|
||||
const connection = createConnection()
|
||||
return () => connection.close()
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
The framework tracks and disposes all of these operations:
|
||||
- `ctx.on(event, handler)` — event listener
|
||||
- `ctx.tools.register(tool)` — tool registration
|
||||
- `ctx.llm.registerAdapter(names, adapter)` — LLM adapter registration
|
||||
- `ctx.effect(() => cleanup)` — custom resource
|
||||
|
||||
During unload, disposer invocation starts in reverse registration order, but multiple async disposers run concurrently and have no serial completion guarantee. Put order-dependent cleanup in one disposer returned from a single `ctx.effect()` and await its steps serially there.
|
||||
|
||||
## Nested contexts
|
||||
|
||||
`ctx.plugin()` creates a child Fiber that inherits the parent context but has an independent lifecycle:
|
||||
|
||||
```ts ignore-check
|
||||
export function apply(ctx: Context) {
|
||||
// Register a child plugin.
|
||||
ctx.plugin(childPlugin)
|
||||
|
||||
// The child has its own Fiber and unloads with its parent.
|
||||
}
|
||||
```
|
||||
|
||||
## Dispose semantics
|
||||
|
||||
To stop a plugin instance early:
|
||||
|
||||
```ts ignore-check
|
||||
const fiber = ctx.plugin(myPlugin)
|
||||
|
||||
// Dispose it manually later.
|
||||
fiber.dispose()
|
||||
```
|
||||
|
||||
`dispose` guarantees:
|
||||
1. All registrations owned by the plugin are removed.
|
||||
2. Child plugins are recursively unloaded.
|
||||
3. The returned promise resolves after all asynchronous cleanup finishes.
|
||||
|
||||
## Hot replacement (HMR)
|
||||
|
||||
With `@cordisjs/plugin-hmr` loaded from `cordis.yml`, editing a plugin source file triggers:
|
||||
|
||||
1. Unload the old plugin and clean up its registrations.
|
||||
2. Load the new code.
|
||||
3. Run the new `apply`.
|
||||
|
||||
Because plugin registrations clean themselves up, hot replacement does not retain registrations from the old instance.
|
||||
|
||||
## Example lifecycle
|
||||
|
||||
```ts ignore-check
|
||||
export function apply(ctx: Context) {
|
||||
console.log('plugin loading')
|
||||
|
||||
ctx.effect(() => {
|
||||
console.log('effect registered')
|
||||
return () => console.log('effect cleaned up')
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Loading prints:
|
||||
```
|
||||
plugin loading
|
||||
effect registered
|
||||
```
|
||||
|
||||
Unloading prints:
|
||||
```
|
||||
effect cleaned up
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Services and dependencies](./service.md) — expose a capability to other plugins
|
||||
- [Event system](./events.md) — communicate between plugins
|
||||
@@ -0,0 +1,131 @@
|
||||
# 插件与生命周期
|
||||
|
||||
[English](index.md) | 中文
|
||||
|
||||
深入了解 Cordis 插件模型和生命周期状态机。
|
||||
|
||||
## Fiber 状态机
|
||||
|
||||
每个被加载的插件对应一个 **Fiber**(作用域)。Fiber 有以下状态:
|
||||
|
||||
```
|
||||
PENDING → LOADING → ACTIVE
|
||||
↘ FAILED
|
||||
ACTIVE → UNLOADING → DISPOSED
|
||||
```
|
||||
|
||||
| 状态 | 含义 |
|
||||
|------|------|
|
||||
| PENDING | 已声明但依赖未就绪 |
|
||||
| LOADING | 依赖就绪,正在执行 `apply` |
|
||||
| ACTIVE | 插件运行中 |
|
||||
| FAILED | `apply` 抛出异常 |
|
||||
| UNLOADING | 正在卸载,清理中 |
|
||||
| DISPOSED | 已完全卸载 |
|
||||
|
||||
## 依赖驱动的加载
|
||||
|
||||
声明了 `inject` 的插件不会立即加载,而是等待依赖的服务就绪:
|
||||
|
||||
```ts ignore-check
|
||||
export const inject = ['tools', 'llm']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// ctx.tools and ctx.llm are ready here.
|
||||
}
|
||||
```
|
||||
|
||||
如果依赖的服务消失(比如提供者被热替换),插件会被自动卸载(ACTIVE → DISPOSED),待服务恢复后重新加载。
|
||||
|
||||
## 自动清理机制
|
||||
|
||||
通过 `ctx` 做的任何注册,在插件卸载时都会自动撤销:
|
||||
|
||||
```ts ignore-check
|
||||
export function apply(ctx: Context) {
|
||||
// Event listener: removed automatically on unload.
|
||||
ctx.on('some-event', handler)
|
||||
|
||||
// Custom resource: the returned disposer runs on unload.
|
||||
ctx.effect(() => {
|
||||
const connection = createConnection()
|
||||
return () => connection.close()
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
以下操作都会被自动追踪和清理:
|
||||
- `ctx.on(event, handler)` — 事件监听
|
||||
- `ctx.tools.register(tool)` — tool 注册
|
||||
- `ctx.llm.registerAdapter(names, adapter)` — LLM 适配器注册
|
||||
- `ctx.effect(() => cleanup)` — 自定义资源
|
||||
|
||||
插件卸载时,处置器按注册顺序的反向发起,但多个异步处置器会并发执行,不保证逐个完成。存在顺序依赖的清理步骤必须放进同一个 `ctx.effect()` 返回的处置器中,由该处置器负责串行等待。
|
||||
|
||||
## 嵌套上下文
|
||||
|
||||
`ctx.plugin()` 创建子 Fiber,它继承父上下文但有独立的生命周期:
|
||||
|
||||
```ts ignore-check
|
||||
export function apply(ctx: Context) {
|
||||
// Register a child plugin.
|
||||
ctx.plugin(childPlugin)
|
||||
|
||||
// The child has its own Fiber and unloads with its parent.
|
||||
}
|
||||
```
|
||||
|
||||
## dispose 语义
|
||||
|
||||
当你需要提前终止一个插件实例:
|
||||
|
||||
```ts ignore-check
|
||||
const fiber = ctx.plugin(myPlugin)
|
||||
|
||||
// Dispose it manually later.
|
||||
fiber.dispose()
|
||||
```
|
||||
|
||||
`dispose` 保证:
|
||||
1. 该插件注册的所有东西被撤销
|
||||
2. 它的子插件也被递归卸载
|
||||
3. 所有异步清理完成后 Promise resolve
|
||||
|
||||
## 热替换 (HMR)
|
||||
|
||||
在开发环境中(`cordis.yml` 加载了 `@cordisjs/plugin-hmr`),修改插件源文件会自动触发:
|
||||
|
||||
1. 卸载旧插件(清理所有注册)
|
||||
2. 重新加载新代码
|
||||
3. 执行新的 `apply`
|
||||
|
||||
因为所有注册都会被自动清理,所以热替换天然安全——不会留下旧状态。
|
||||
|
||||
## 实战:理解生命周期
|
||||
|
||||
```ts ignore-check
|
||||
export function apply(ctx: Context) {
|
||||
console.log('plugin loading')
|
||||
|
||||
ctx.effect(() => {
|
||||
console.log('effect registered')
|
||||
return () => console.log('effect cleaned up')
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
加载时输出:
|
||||
```
|
||||
plugin loading
|
||||
effect registered
|
||||
```
|
||||
|
||||
卸载时输出:
|
||||
```
|
||||
effect cleaned up
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- [服务与依赖](./service.md) — 让你的插件对外提供能力
|
||||
- [事件系统](./events.md) — 插件间通信的核心机制
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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
|
||||
service.md: 1bf28cb3c7dfdfbd6d0babfa3b1688ac65eea01e
|
||||
service.zh.md: 17785c056ab9a0a21974e6ed8bbe7f7de05fa00e
|
||||
@@ -0,0 +1,148 @@
|
||||
# Services and dependencies
|
||||
|
||||
English | [中文](service.zh.md)
|
||||
|
||||
A service is a capability one plugin exposes to other plugins. `inject` declares the services a plugin requires.
|
||||
|
||||
## What is a service?
|
||||
|
||||
In Harness, `tools`, `llm`, and `agents` are services. Each is a named capability mounted on `ctx`:
|
||||
|
||||
```ts ignore-check
|
||||
ctx.tools // ToolRegistry service
|
||||
ctx.llm // LLM service
|
||||
ctx.agents // Agent service
|
||||
```
|
||||
|
||||
Any plugin can provide a service for other plugins to consume.
|
||||
|
||||
## Consume a service
|
||||
|
||||
Declare `inject` to use an existing service:
|
||||
|
||||
```ts ignore-check
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// ctx.tools exists and is ready here.
|
||||
ctx.tools.register(/* ... */)
|
||||
}
|
||||
```
|
||||
|
||||
When `apply` runs, every service declared by `inject` is ready. If a service is not ready, the plugin waits instead of running.
|
||||
|
||||
## Provide a service
|
||||
|
||||
### Extend Service
|
||||
|
||||
```ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
|
||||
export default class MetricsService extends Service {
|
||||
static inject = ['llm'] // A service may depend on other services.
|
||||
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'metrics') // 'metrics' is the service name.
|
||||
}
|
||||
|
||||
// Public service method.
|
||||
record(event: string, value: number) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After loading this plugin, consumers access the service as `ctx.metrics`:
|
||||
|
||||
```ts ignore-check
|
||||
export const inject = ['metrics']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.metrics.record('tool_call', 1)
|
||||
}
|
||||
```
|
||||
|
||||
### Declare its type
|
||||
|
||||
Use TypeScript declaration merging to type `ctx.metrics`:
|
||||
|
||||
```ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
metrics: MetricsService
|
||||
}
|
||||
}
|
||||
|
||||
export default class MetricsService extends Service {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'metrics')
|
||||
}
|
||||
|
||||
record(event: string, value: number) { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency behavior
|
||||
|
||||
### Required and optional dependencies
|
||||
|
||||
```ts ignore-check
|
||||
// Required: the plugin does not load while the service is absent.
|
||||
export const inject = ['tools']
|
||||
|
||||
// Optional: omit inject and query with ctx.get() at the use site.
|
||||
export function apply(ctx: Context) {
|
||||
const metrics = ctx.get('metrics')
|
||||
metrics?.record('plugin_loaded', 1)
|
||||
}
|
||||
```
|
||||
|
||||
### When a service disappears
|
||||
|
||||
If a required service disappears while the application is running, for example because its provider unloads:
|
||||
|
||||
1. Dependent plugins dispose automatically.
|
||||
2. They load again when the service returns.
|
||||
|
||||
This prevents a plugin from calling a service that no longer exists.
|
||||
|
||||
## Service isolation
|
||||
|
||||
`cordis.yml` can isolate services so separate plugin groups see separate instances of the same service:
|
||||
|
||||
```yaml
|
||||
- id: group-a
|
||||
name: '@cordisjs/plugin-group'
|
||||
group: true
|
||||
isolate:
|
||||
bash: true
|
||||
config:
|
||||
- name: '@deepseek-ai/dsh-bash-local'
|
||||
config:
|
||||
timeoutMs: 5000
|
||||
- name: './src/plugin-a.ts'
|
||||
|
||||
- id: group-b
|
||||
name: '@cordisjs/plugin-group'
|
||||
group: true
|
||||
isolate:
|
||||
bash: true
|
||||
config:
|
||||
- name: '@deepseek-ai/dsh-bash-local'
|
||||
config:
|
||||
timeoutMs: 60000
|
||||
- name: './src/plugin-b.ts'
|
||||
```
|
||||
|
||||
`plugin-a` and `plugin-b` each see the Bash instance in their own group, with no cross-group effect.
|
||||
|
||||
## Built-in Harness services
|
||||
|
||||
The repository generates the service names, public methods, and source locations in the [service catalog](../../../cordis-catalog/services.md). Use that catalog and the service's TypeScript interface while developing a plugin; do not maintain a second static list.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Event system](./events.md) — communicate between plugins without tight coupling
|
||||
- [Capability layering](../practice/) — use services as capability interfaces
|
||||
@@ -0,0 +1,148 @@
|
||||
# 服务与依赖
|
||||
|
||||
[English](service.md) | 中文
|
||||
|
||||
服务 (Service) 是插件对外暴露能力的方式。依赖 (inject) 是插件声明自己需要哪些服务。
|
||||
|
||||
## 什么是服务
|
||||
|
||||
在 Harness 中,`tools`、`llm`、`agents` 都是服务。服务是挂载在 `ctx` 上的命名能力:
|
||||
|
||||
```ts ignore-check
|
||||
ctx.tools // ToolRegistry service
|
||||
ctx.llm // LLM service
|
||||
ctx.agents // Agent service
|
||||
```
|
||||
|
||||
任何插件都可以提供一个新服务,供其他插件使用。
|
||||
|
||||
## 使用服务
|
||||
|
||||
声明 `inject` 来使用已有服务:
|
||||
|
||||
```ts ignore-check
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// ctx.tools exists and is ready here.
|
||||
ctx.tools.register(/* ... */)
|
||||
}
|
||||
```
|
||||
|
||||
框架保证:在 `apply` 执行时,`inject` 声明的服务已经全部就绪。如果服务还没准备好,你的插件会等着,不会执行。
|
||||
|
||||
## 提供服务
|
||||
|
||||
### 使用 Service 基类
|
||||
|
||||
```ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
|
||||
export default class MetricsService extends Service {
|
||||
static inject = ['llm'] // A service may depend on other services.
|
||||
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'metrics') // 'metrics' is the service name.
|
||||
}
|
||||
|
||||
// Public service method.
|
||||
record(event: string, value: number) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
加载这个插件后,其他插件就可以通过 `ctx.metrics` 访问它:
|
||||
|
||||
```ts ignore-check
|
||||
export const inject = ['metrics']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.metrics.record('tool_call', 1)
|
||||
}
|
||||
```
|
||||
|
||||
### 类型声明
|
||||
|
||||
使用 TypeScript 声明合并让 `ctx.metrics` 有正确类型:
|
||||
|
||||
```ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
metrics: MetricsService
|
||||
}
|
||||
}
|
||||
|
||||
export default class MetricsService extends Service {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'metrics')
|
||||
}
|
||||
|
||||
record(event: string, value: number) { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
## 依赖的行为
|
||||
|
||||
### 必选依赖 vs 可选依赖
|
||||
|
||||
```ts ignore-check
|
||||
// Required: the plugin does not load while the service is absent.
|
||||
export const inject = ['tools']
|
||||
|
||||
// Optional: omit inject and query with ctx.get() at the use site.
|
||||
export function apply(ctx: Context) {
|
||||
const metrics = ctx.get('metrics')
|
||||
metrics?.record('plugin_loaded', 1)
|
||||
}
|
||||
```
|
||||
|
||||
### 服务消失时的行为
|
||||
|
||||
如果一个必选依赖的服务在运行时消失(比如提供者被卸载):
|
||||
|
||||
1. 依赖它的插件自动 dispose
|
||||
2. 当服务重新出现时,插件自动重新加载
|
||||
|
||||
这保证了不会出现"调用一个已不存在的服务"的情况。
|
||||
|
||||
## 服务隔离
|
||||
|
||||
`cordis.yml` 支持服务隔离——同一个服务可以有多个实例,不同插件组看到不同实例:
|
||||
|
||||
```yaml
|
||||
- id: group-a
|
||||
name: '@cordisjs/plugin-group'
|
||||
group: true
|
||||
isolate:
|
||||
bash: true
|
||||
config:
|
||||
- name: '@deepseek-ai/dsh-bash-local'
|
||||
config:
|
||||
timeoutMs: 5000
|
||||
- name: './src/plugin-a.ts'
|
||||
|
||||
- id: group-b
|
||||
name: '@cordisjs/plugin-group'
|
||||
group: true
|
||||
isolate:
|
||||
bash: true
|
||||
config:
|
||||
- name: '@deepseek-ai/dsh-bash-local'
|
||||
config:
|
||||
timeoutMs: 60000
|
||||
- name: './src/plugin-b.ts'
|
||||
```
|
||||
|
||||
`plugin-a` 和 `plugin-b` 各自看到自己组内的 bash 实例,互不影响。
|
||||
|
||||
## Harness 内置服务
|
||||
|
||||
服务名、公开方法和源码位置由仓库自动生成,见[服务目录](../../../cordis-catalog/services.md)。开发插件时应以该目录和服务接口的 TypeScript 类型为准,不要复制一份静态清单。
|
||||
|
||||
## 下一步
|
||||
|
||||
- [事件系统](./events.md) — 插件间松耦合通信
|
||||
- [能力三件套](../practice/) — 服务在 seam 模式中的应用
|
||||
Reference in New Issue
Block a user