docs: translate remaining non-README documentation
This commit is contained in:
6
docs/cookbook/maintaining-dsh-code-review.i18n.yaml
Normal file
6
docs/cookbook/maintaining-dsh-code-review.i18n.yaml
Normal file
@@ -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
|
||||
maintaining-dsh-code-review.md: 2b5d0d926ae922f2650daac33cf35991cb71c5e5
|
||||
maintaining-dsh-code-review.zh.md: c0e8b64fde3a67174878b4b0665712c9ba2e67c0
|
||||
@@ -1,5 +1,7 @@
|
||||
# Maintaining the dsh-code-review skill
|
||||
|
||||
English | [中文](maintaining-dsh-code-review.zh.md)
|
||||
|
||||
The [`dsh-code-review`](../../.agents/skills/dsh-code-review/SKILL.md) skill is kept current by a single designated operator running a private periodic maintenance tool. This cookbook is the entry point for that operator — and for anyone taking over the role — and for repo contributors who want to understand why skill updates arrive as small periodic PRs rather than one-off audits. The workflow itself is specified in the [human-review skill-maintenance Agent Note](../../.agents/notes/proposed/process/2026-07-13-human-review-skill-maintenance.md).
|
||||
|
||||
## What the maintainer receives
|
||||
|
||||
64
docs/cookbook/maintaining-dsh-code-review.zh.md
Normal file
64
docs/cookbook/maintaining-dsh-code-review.zh.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# 维护 dsh-code-review skill
|
||||
|
||||
[English](maintaining-dsh-code-review.md) | 中文
|
||||
|
||||
[`dsh-code-review`](../../.agents/skills/dsh-code-review/SKILL.md) skill(技能)由一名指定操作员通过私有的周期维护工具持续更新。本实操手册(cookbook)既是该操作员和接任者的入口,也帮助仓库贡献者理解为何 skill 更新会以小型周期 PR(Pull Request)的形式出现,而不是一次性审计。工作流本身由[人工评审 skill 维护 Agent Note(agent 决策记录)](../../.agents/notes/proposed/process/2026-07-13-human-review-skill-maintenance.md)规定。
|
||||
|
||||
## 维护者会收到什么
|
||||
|
||||
每天运行私有工具,并使用 2 个 UTC 日的重叠窗口;在拟议的调度器完成验收运行之前,操作员按相同频率手动调用包装脚本。每周手动恢复运行使用 7 日窗口。工作流会:
|
||||
|
||||
1. 选择指定窗口内合并、且合并 commit 可从 `origin/master` 到达的 PR(每天运行默认选择 2 个 UTC 日,每周运行选择 7 日)。合并 commit 无法到达的 PR(例如父分支被 squash 的堆叠分支),或超出 250 个 commit 获取上限的 PR,会记录到 `skipped-pulls.json` 并跳过,不会中止本次运行。
|
||||
2. 收集合并前带 commit 锚点的人工评审反馈(行内评论和评审提交),然后比较反馈时与最终落地的 PR patch。它不获取 PR 会话评论,因为 GitHub 当前状态无法为这些评论提供可抵抗 force-push 的反馈时基线;它也不会把只存在于目标分支的变更作为采纳证据。
|
||||
3. 两个独立配置的评审适配器先对来源和采纳情况分类,再根据当前 skill 对双方一致认定已采纳的条目分类。
|
||||
4. 主适配器起草完整修订版 `SKILL.md`;两个适配器评审同一份 diff;只要仍有阻塞发现,循环就会继续,直到双方批准。
|
||||
5. 工具声明成功前,会针对候选版本运行 `pnpm run doc-sync` 和 `pnpm run lint`。
|
||||
|
||||
每次运行都把产物保存在操作员的机器上。保存的 diff、候选 `SKILL.md` 和提升 manifest(元数据清单)按时间戳命名,存放在 `~/dsh-code-review-outputs/` 下。manifest 记录源 master commit 与 skill blob、源反馈 ID 和 URL、已落地证据范围、适配器裁决和门禁结果;每个适配器的原始 I/O 留在私有临时目录中,该目录路径会写入通知和 `~/Library/Logs/dsh-code-review-maintainer/` 下的每日日志。维护 worktree 在每次运行后都会恢复为干净状态,避免操作员直接在维护副本中编辑。
|
||||
|
||||
## 操作员如何处理候选 diff
|
||||
|
||||
某次运行产出候选版本时,macOS 会发出一条带 `dsh-code-review-promote <timestamp>` 提示的通知。
|
||||
|
||||
1. **根据 diff 本身作出判断。** 不要因为「评审者已经批准」就直接接受:维护者契约规定最终判断由操作员作出。检查清单是否膨胀、是否有历史叙述、是否根据单次事件作出无依据的外推,以及是否与现有 skill 或权威文档重复。
|
||||
|
||||
```sh
|
||||
ls ~/dsh-code-review-outputs/ # every candidate ever produced
|
||||
less ~/dsh-code-review-outputs/2026-07-16T02-00-00Z.diff
|
||||
less ~/dsh-code-review-outputs/2026-07-16T02-00-00Z.SKILL.md
|
||||
less ~/dsh-code-review-outputs/2026-07-16T02-00-00Z.manifest.json
|
||||
```
|
||||
|
||||
2. **与运行产物交叉核验。** 提升 manifest 会把每条拟议规则映射到源反馈和已落地证据;每个适配器的详细 I/O、共识和采纳证据位于本次运行的私有临时目录中(路径见日志)。至少抽查一个候选项:链接的人工评论是否确实支持新增规则?链接的 PR 是否确实采纳了它?
|
||||
|
||||
3. **从三种处理方式中选择一种:**
|
||||
- **丢弃。** 删除保存的候选版本。下一次运行会依据届时的当前 skill,重新考虑同一份反馈。
|
||||
|
||||
```sh
|
||||
rm ~/dsh-code-review-outputs/2026-07-16T02-00-00Z.{diff,SKILL.md,manifest.json}
|
||||
```
|
||||
- **暂存成批。** 如果更新很小,可以把候选版本留待与后续版本合并。源 skill 检查仍然适用;如果 `master` 先发生变化,请重新运行分析,或手动 rebase 并重新评审 diff。
|
||||
- **提升。** 在仓库的干净 `master` checkout 中运行提升辅助工具。它会刷新 `master`、验证当前 skill 与记录的源 blob 一致、应用保存的 diff,并创建一份 draft PR,其正文包含 manifest 的来源摘要。如果 skill 已发生漂移,它会停止而不是覆盖更新后的指导;操作员仍需在 GitHub 上评审 PR,并选择合并或关闭。
|
||||
|
||||
```sh
|
||||
cd ~/path/to/deepseek-harness # clean master
|
||||
dsh-code-review-promote 2026-07-16T02-00-00Z
|
||||
```
|
||||
|
||||
4. **不要逐字提交适配器输出。** 提升过程中可以进行小幅编辑,例如收紧措辞、移除只有结合源 PR 上下文才有意义的示例、把规则并入现有规则。这些编辑是预期行为,也保留了工作流所依赖的「评审者判断」。合并前应在该分支上修订这些改动。
|
||||
|
||||
## 运行未产出候选版本时
|
||||
|
||||
只要每个非空分类阶段都至少产生一个有效的适配器结果,这就是常见情况。工具会在每日日志中记录「无候选版本」,不发送通知(避免提醒疲劳),然后继续。某天没有 skill 更新,说明工作流运行正常,而不是停滞。
|
||||
|
||||
## 中断与交接
|
||||
|
||||
该机制运行在一台机器上。操作员应随时处理以下中断:
|
||||
|
||||
- **错过每日运行。** 2 日重叠窗口会自动覆盖一次漏跑;更长的间隔可通过设置 `DSH_CODE_REVIEW_SINCE=<Nd>` 手动运行包装脚本来恢复。重叠窗口具有幂等性:当前 skill 已包含的指导会被归类为 `covered`,不会再次成为候选项。
|
||||
- **适配器提供方中断。** 当两个评审命令解析为逐字节相同的可执行文件时,工具会拒绝运行。某个批次的适配器响应未通过 schema 或 ID 校验时,该批次会整体 fail-closed(其中每个条目都标记为不明确),运行则继续;原始输出会保留以便调试。如果任一适配器在某项操作的所有非空批次中都未产生有效结果,本次运行就会失败、写入失败记录并通知操作员;它绝不会把提供方完全中断折叠成「无候选版本」。
|
||||
- **交接给另一名维护者。** 新建一篇取代当前记录的后续 Agent Note:要么把机制移入仓库,要么记录新操作员的私有设置。不要暗中转交工具;Agent Note 的风险章节已把「单维护者关键人风险」列为交接必须记录决策的原因。
|
||||
|
||||
## 操作员的私有设置位于何处
|
||||
|
||||
工具源代码、评审适配器、提供方凭据和调度器属于操作员的私有基础设施,按设计位于本仓库之外(参见 Agent Note 的「机制位于何处」章节)。本实操手册和 Agent Note 描述的是**工作流保证什么**;这些保证**如何**实现则属于私有基础设施问题。如果你是新操作员,应以 Agent Note 的 `## Proposal` 各节作为实现依据。
|
||||
6
docs/cordis-tutorial/01-first-plugin.i18n.yaml
Normal file
6
docs/cordis-tutorial/01-first-plugin.i18n.yaml
Normal file
@@ -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
|
||||
01-first-plugin.md: b730b7ad7dc9ebd2e5dc8af4f7a83bd58ac7d4d8
|
||||
01-first-plugin.zh.md: 1e6901c048f5268ddead0faca85eaf5eef9c7533
|
||||
@@ -1,5 +1,7 @@
|
||||
# 1. Your first plugin
|
||||
|
||||
English | [中文](01-first-plugin.zh.md)
|
||||
|
||||
In the loader configuration used here, a Cordis plugin module named-exports an `apply` function. When Cordis loads it, it calls `apply` with a **context** — the `ctx` object through which the plugin registers everything it contributes.
|
||||
|
||||
## Write the plugin
|
||||
|
||||
95
docs/cordis-tutorial/01-first-plugin.zh.md
Normal file
95
docs/cordis-tutorial/01-first-plugin.zh.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# 1. 编写第一个插件
|
||||
|
||||
[English](01-first-plugin.md) | 中文
|
||||
|
||||
在本教程使用的 loader 配置中,Cordis 插件模块通过命名导出提供 `apply` 函数。Cordis 加载模块时,会用一个 **上下文** 调用 `apply`;该上下文就是 `ctx` 对象,插件通过它注册自己贡献的所有内容。
|
||||
|
||||
## 编写插件
|
||||
|
||||
在 `tmp/cordis-tutorial` 目录中(参见[环境设置](index.md#setup))创建 `hello.ts`:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
export const name = 'hello'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
console.log('hello from my first plugin')
|
||||
}
|
||||
```
|
||||
|
||||
`name` 导出项是可选的显示元数据;它用于在诊断信息中标识插件。
|
||||
|
||||
## 组合应用
|
||||
|
||||
本教程的启动器通过配置组装应用。创建 `cordis.yml`:
|
||||
|
||||
```yaml
|
||||
- name: './hello.ts'
|
||||
```
|
||||
|
||||
该文件是一组 Cordis 配置项的列表。`name` 是模块指定符,可以是相对路径或 NPM 包(package)名;loader 会挂载每个配置项。各项会并发启动,因此它们在列表中的位置不保证插件的加载先后;顺序由服务依赖(`inject`,参见[第 3 章](03-services.md))决定,而非文件中的位置。
|
||||
|
||||
## 运行
|
||||
|
||||
```sh
|
||||
node --import tsx ../../vendor/cordis/bin.js
|
||||
```
|
||||
|
||||
预期输出:
|
||||
|
||||
```
|
||||
hello from my first plugin
|
||||
```
|
||||
|
||||
当没有任何内容继续运行时,进程会自行退出。具体过程如下:
|
||||
|
||||
1. 启动器创建根 `Context`,并挂载 **Loader** 插件。
|
||||
2. Loader 读取 `cordis.yml`,解析 `./hello.ts`,然后将其作为子插件挂载。
|
||||
3. Cordis 调用你的 `apply(ctx)`。
|
||||
|
||||
你的文件中没有框架启动代码:插件描述自己的贡献,`cordis.yml` 则组合应用。例如,[TUI agent(智能体)](../../examples/tui-agent/cordis.yml) 就是一个更长的插件组合。
|
||||
|
||||
## 其他两种插件形态
|
||||
|
||||
函数是最常见的形态,但 Cordis 接受三种形态:
|
||||
|
||||
```ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
|
||||
// 1. Function plugin (what you just wrote).
|
||||
export function apply(ctx: Context) {}
|
||||
|
||||
// 2. Object plugin: an object with an `apply` method.
|
||||
export const objectPlugin = {
|
||||
name: 'object-plugin',
|
||||
apply(ctx: Context) {},
|
||||
}
|
||||
|
||||
// 3. Class plugin: a Service subclass (covered in chapter 3).
|
||||
export class MyService extends Service {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'myTutorialService')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在你需要公开服务之前,请一直使用函数形态;[第 3 章](03-services.md)介绍了何时应当使用类形态。
|
||||
|
||||
## 尝试制造错误
|
||||
|
||||
让 `apply` 抛出异常:
|
||||
|
||||
```ts ignore-check
|
||||
export function apply(ctx: Context) {
|
||||
throw new Error('apply exploded')
|
||||
}
|
||||
```
|
||||
|
||||
再次运行:进程会因该错误而终止。插件加载失败必须明确报错,不会仅跳过该配置项。
|
||||
|
||||
还需要尽早了解一个例外:如果某个配置项的模块无法被 **解析**,例如路径或包名拼写错误,Cordis 会通过 logger 服务报告错误,而不会使进程崩溃。在启动阶段,这条报告可能在 console 导出器开始观察之前丢失。如果新增配置项似乎没有任何效果,请先检查拼写。
|
||||
|
||||
下一章:[生命周期与 effect](02-lifecycle-and-effects.md):插件卸载时会发生什么。
|
||||
|
||||
[](https://github.com/deepseek-harness/deepseek-harness)
|
||||
6
docs/cordis-tutorial/02-lifecycle-and-effects.i18n.yaml
Normal file
6
docs/cordis-tutorial/02-lifecycle-and-effects.i18n.yaml
Normal file
@@ -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
|
||||
02-lifecycle-and-effects.md: f1b39e06e9d25c51ab2d76503025e2b6ffe90c73
|
||||
02-lifecycle-and-effects.zh.md: a6021ed7475a0045d480810747244274eb5b4198
|
||||
@@ -1,5 +1,7 @@
|
||||
# 2. Lifecycle and effects
|
||||
|
||||
English | [中文](02-lifecycle-and-effects.zh.md)
|
||||
|
||||
A Cordis plugin can be unloaded by a config edit, hot reload, explicit disposal, or loss of a required service. Registrations made through Cordis APIs are effects and are undone when their owning plugin unloads; resources managed outside those APIs must be wrapped in `ctx.effect()`.
|
||||
|
||||
## Effects
|
||||
|
||||
98
docs/cordis-tutorial/02-lifecycle-and-effects.zh.md
Normal file
98
docs/cordis-tutorial/02-lifecycle-and-effects.zh.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# 2. 生命周期与 effect
|
||||
|
||||
[English](02-lifecycle-and-effects.md) | 中文
|
||||
|
||||
Cordis 插件可能因配置编辑、热重载、显式资源释放或所需服务消失而卸载。通过 Cordis API 建立的注册属于 effect,会在所属插件卸载时撤销;在这些 API 之外管理的资源必须包装在 `ctx.effect()` 中。
|
||||
|
||||
## Effect
|
||||
|
||||
对于 Cordis 尚未管理的资源,例如定时器、连接或 watcher,应将其包装在 `ctx.effect()` 中并返回 disposer(dispose(资源释放)函数):
|
||||
|
||||
创建 `lifecycle.ts`,将它放在 `tmp/cordis-tutorial` 中:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
export const name = 'lifecycle-demo'
|
||||
|
||||
function heartbeat(ctx: Context) {
|
||||
console.log('heartbeat plugin loading')
|
||||
ctx.effect(() => {
|
||||
const timer = setInterval(() => console.log('tick'), 200)
|
||||
return () => {
|
||||
clearInterval(timer)
|
||||
console.log('heartbeat cleaned up')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// Mount a child plugin and keep its fiber to dispose it later.
|
||||
const fiber = ctx.plugin(heartbeat)
|
||||
// The demo timer is itself an effect: if THIS plugin is unloaded first,
|
||||
// the pending callback is cancelled instead of firing on a dead app.
|
||||
ctx.effect(() => {
|
||||
const timer = setTimeout(async () => {
|
||||
await fiber.dispose()
|
||||
console.log('disposed')
|
||||
process.exit(0)
|
||||
}, 700)
|
||||
return () => clearTimeout(timer)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
让 `cordis.yml` 指向该文件:
|
||||
|
||||
```yaml
|
||||
- name: './lifecycle.ts'
|
||||
```
|
||||
|
||||
运行(`node --import tsx ../../vendor/cordis/bin.js`)后会得到:
|
||||
|
||||
```
|
||||
heartbeat plugin loading
|
||||
tick
|
||||
tick
|
||||
tick
|
||||
heartbeat cleaned up
|
||||
disposed
|
||||
```
|
||||
|
||||
请留意三点:
|
||||
|
||||
- `ctx.plugin(heartbeat)` 会把一个**来自代码**的函数挂载为插件,这与 YAML loader 为每个配置项执行的操作相同。函数插件不需要 `apply` 方法:Cordis 会直接调用该函数,其名称只用于诊断。只有对象形态才要求 `apply` 方法,例如 `ctx.plugin({ apply(ctx) { /* ... */ } })`。调用会返回一个 **fiber**,即一个已加载插件实例的运行时句柄。
|
||||
- effect 主体在加载期间运行;它返回的 disposer 在卸载期间运行。对于生命周期与插件一致的资源,你绝不需要自行调用 disposer。
|
||||
- `fiber.dispose()` 会等该插件的所有清理工作(包括异步 disposer)完成后才结束,并递归卸载它挂载的所有子插件。
|
||||
|
||||
## Fiber 状态机
|
||||
|
||||
每个已加载插件实例都拥有一个 fiber,并依次经过以下状态:
|
||||
|
||||
```
|
||||
PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
|
||||
↘ FAILED
|
||||
```
|
||||
|
||||
- **PENDING**:已经声明,但所需服务(第 3 章)尚不可用。
|
||||
- **LOADING / ACTIVE**:`apply` 正在运行/已经完成。
|
||||
- **FAILED**:`apply` 或配置校验抛出异常。
|
||||
- **UNLOADING / DISPOSED**:disposer 正在运行/一切均已拆除。
|
||||
|
||||
你会在[第 6 章](06-composition-and-hmr.md)再次遇到 PENDING,它通常就是「为什么我的插件没有输出」的答案。
|
||||
|
||||
## 已经属于 effect 的操作
|
||||
|
||||
你很少需要亲自编写 `ctx.effect()`,因为内置注册 API 本身已经是 effect:
|
||||
|
||||
- `ctx.on(event, listener)`:监听器会在卸载时移除([第 4 章](04-events.md))。
|
||||
- `ctx.plugin(child)`:子插件会随父插件一同 dispose。
|
||||
- 服务注册属于 effect。`ctx.tools.register(...)` 等 harness 注册表也会把返回的 disposer 附着到调用插件上,因此会自动回卷([第 7 章](07-into-the-harness.md))。
|
||||
|
||||
对于 Cordis 不管理的资源,应在 `ctx.effect()` 内获取它,并返回用于释放资源的 disposer。此后 Cordis 会在卸载期间调用该释放逻辑,热重载时也不例外。
|
||||
|
||||
有一项顺序注意事项:disposer 会按注册顺序的逆序启动,但多个**异步** disposer 会并发运行。如果拆除步骤必须按顺序执行,请把它们放在同一个 disposer 中,并在其中依次等待每步完成。
|
||||
|
||||
下一章:[服务](03-services.md):插件如何共享功能。
|
||||
|
||||
[](https://github.com/deepseek-harness/deepseek-harness)
|
||||
6
docs/cordis-tutorial/03-services.i18n.yaml
Normal file
6
docs/cordis-tutorial/03-services.i18n.yaml
Normal file
@@ -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
|
||||
03-services.md: 5848132c6ad18338fa893954d45fc20005db6199
|
||||
03-services.zh.md: 3c77d0451df9062f1a344e7474e6be141b709197
|
||||
@@ -1,5 +1,7 @@
|
||||
# 3. Services
|
||||
|
||||
English | [中文](03-services.zh.md)
|
||||
|
||||
A **service** is a named capability one plugin provides and other plugins consume through `ctx`. In the harness, `ctx.tools`, `ctx.llm`, and `ctx.agents` are services. A consumer names the capability, such as `'tools'`, rather than importing its provider, so configuration can select a provider without changing the consumer.
|
||||
|
||||
## Provide a service
|
||||
|
||||
98
docs/cordis-tutorial/03-services.zh.md
Normal file
98
docs/cordis-tutorial/03-services.zh.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# 3. 服务
|
||||
|
||||
[English](03-services.md) | 中文
|
||||
|
||||
**服务**是一个插件提供、其他插件通过 `ctx` 消费的命名功能。在 harness 中,`ctx.tools`、`ctx.llm` 和 `ctx.agents` 都是服务。消费方只命名 `'tools'` 之类的功能,而不导入其提供方,因此配置可以选择提供方,无需修改消费方。
|
||||
|
||||
## 提供服务
|
||||
|
||||
创建 `greeter.ts`,将它放在 `tmp/cordis-tutorial` 中:
|
||||
|
||||
```ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
greeter: GreeterService
|
||||
}
|
||||
}
|
||||
|
||||
export class GreeterService extends Service {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'greeter')
|
||||
}
|
||||
|
||||
greet(who: string) {
|
||||
return `Hello, ${who}!`
|
||||
}
|
||||
}
|
||||
|
||||
export const name = 'greeter'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.plugin(GreeterService)
|
||||
}
|
||||
```
|
||||
|
||||
两部分协同工作:
|
||||
|
||||
- **运行时**:`super(ctx, 'greeter')` 以名称 `greeter` 注册该实例。此后,任何插件都可以通过 `ctx.greeter` 访问它。注册属于 effect,卸载提供方时会移除该服务。
|
||||
- **编译时**:`declare module 'cordis'` 块使用 TypeScript 声明合并,把 `greeter` 加入 `Context` 接口,使 `ctx.greeter` 在各处都能通过类型检查。它不会生成代码;没有该声明时,服务在运行时仍能工作,但消费方会失去类型安全。
|
||||
|
||||
`Service` 子类本身就是插件(第 1 章介绍的类形态),因此 `ctx.plugin(GreeterService)` 会像挂载其他插件一样挂载它。
|
||||
|
||||
## 使用 `inject` 消费服务
|
||||
|
||||
创建 `consumer.ts`:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
export const name = 'consumer'
|
||||
export const inject = ['greeter']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
console.log(ctx.greeter.greet('world'))
|
||||
}
|
||||
```
|
||||
|
||||
`inject` 列出该插件需要的服务。Cordis 会让插件保持 PENDING,直到列出的每项服务都存在,因此在 `apply` 内可以保证 `ctx.greeter` 已经就绪。`cordis.yml` 中的加载顺序无关紧要:决定插件何时启动的是依赖关系,而不是文件顺序。
|
||||
|
||||
组合并运行:
|
||||
|
||||
```yaml
|
||||
- name: './greeter.ts'
|
||||
- name: './consumer.ts'
|
||||
```
|
||||
|
||||
```
|
||||
Hello, world!
|
||||
```
|
||||
|
||||
交换 `cordis.yml` 中两行的顺序后重新运行,输出仍然相同。尝试彻底移除 `./greeter.ts`:消费方会保持 PENDING,不输出任何内容,既不崩溃,也不会只运行一部分。处于 PENDING 的 fiber 也不会让 Node 的事件循环保持活跃,因此如果组合中没有其他运行项,进程会静默地以状态码 0 退出。[第 6 章](06-composition-and-hmr.md)介绍如何诊断这种状态。
|
||||
|
||||
## 加载后仍会跟踪依赖关系
|
||||
|
||||
`inject` 并非一次性的启动检查。如果应用运行期间所需服务消失,例如提供方被卸载或热替换,每个依赖插件也会随之卸载,并在服务恢复后再次加载。结合 effect([第 2 章](02-lifecycle-and-effects.md)),这能防止运行中的消费方保留对不可用服务的引用:依赖消失时,它自己的注册也会回卷。
|
||||
|
||||
这也是配置中可以替换服务的原因:卸载 `dsh-bash-local` 配置项,挂载另一个 `bash` 提供方,所有注入 `'bash'` 的插件都会干净地重启并使用新实现。
|
||||
|
||||
## 可选依赖
|
||||
|
||||
`inject` 用于硬性依赖。如果某项功能缺失时插件仍可运行,请跳过 `inject`,并在使用处探测:
|
||||
|
||||
```ts ignore-check
|
||||
export function apply(ctx: Context) {
|
||||
// undefined when no provider is loaded; the plugin still runs.
|
||||
const greeter = ctx.get('greeter')
|
||||
console.log(greeter?.greet('maybe') ?? 'no greeter available')
|
||||
}
|
||||
```
|
||||
|
||||
## 命名
|
||||
|
||||
每个应用中的服务名称共用一个扁平命名空间。请为自有服务添加有辨识度的前缀或命名空间(harness 已占用 `tools` 和 `llm` 等普通名称);生成的[服务目录](../cordis-catalog/services.md)列出 harness 注册的每个名称。
|
||||
|
||||
下一章:[事件](04-events.md):无需共享服务即可通信。
|
||||
|
||||
[](https://github.com/deepseek-harness/deepseek-harness)
|
||||
6
docs/cordis-tutorial/04-events.i18n.yaml
Normal file
6
docs/cordis-tutorial/04-events.i18n.yaml
Normal file
@@ -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
|
||||
04-events.md: 18f39dc1b693e5fb7e1793ec4b7dcac9cf24db95
|
||||
04-events.zh.md: f55a61ff2f43ea42968893d07eb92ea0613b921a
|
||||
@@ -1,5 +1,7 @@
|
||||
# 4. Events
|
||||
|
||||
English | [中文](04-events.zh.md)
|
||||
|
||||
Services support direct calls; **events** let a plugin announce something without knowing which plugins listen. The harness uses events for interactions such as tool results, model requests, and approval decisions.
|
||||
|
||||
## Declare, emit, listen
|
||||
|
||||
144
docs/cordis-tutorial/04-events.zh.md
Normal file
144
docs/cordis-tutorial/04-events.zh.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# 4. 事件
|
||||
|
||||
[English](04-events.md) | 中文
|
||||
|
||||
服务支持直接调用;**事件**让插件无需知道有哪些插件正在监听,就能发出通知。harness 使用事件处理工具结果、模型请求和审批决定等交互。
|
||||
|
||||
## 声明、发出与监听
|
||||
|
||||
创建 `stats.ts`,将它放在 `tmp/cordis-tutorial` 中。它是一项负责计数并在每次变化时发出通知的服务:
|
||||
|
||||
```ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
stats: StatsService
|
||||
}
|
||||
interface Events {
|
||||
'stats/report'(name: string, count: number): void
|
||||
}
|
||||
}
|
||||
|
||||
export class StatsService extends Service {
|
||||
private counts = new Map<string, number>()
|
||||
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'stats')
|
||||
}
|
||||
|
||||
bump(name: string) {
|
||||
const next = (this.counts.get(name) ?? 0) + 1
|
||||
this.counts.set(name, next)
|
||||
this.ctx.emit('stats/report', name, next)
|
||||
}
|
||||
}
|
||||
|
||||
export const name = 'stats'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.plugin(StatsService)
|
||||
}
|
||||
```
|
||||
|
||||
`interface Events` 合并与第 3 章的 `interface Context` 合并在事件系统中相互对应:它声明事件名称及其监听器签名,因此 `ctx.emit` 和 `ctx.on` 都具有完整类型。`namespace/action` 命名约定让扁平的事件命名空间保持易读。
|
||||
|
||||
创建 `reporter.ts`:
|
||||
|
||||
```ts ignore-check
|
||||
import type { Context } from 'cordis'
|
||||
import type {} from './stats.ts'
|
||||
|
||||
export const name = 'reporter'
|
||||
export const inject = ['stats']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.on('stats/report', (name, count) => {
|
||||
console.log(`[stats] ${name} -> ${count}`)
|
||||
})
|
||||
ctx.stats.bump('tool_call')
|
||||
ctx.stats.bump('tool_call')
|
||||
ctx.stats.bump('prompt')
|
||||
}
|
||||
```
|
||||
|
||||
`import type {} from './stats.ts'` 行不会在运行时导入任何内容;它的作用是让 TypeScript 看到声明合并。组合并运行:
|
||||
|
||||
```yaml
|
||||
- name: './stats.ts'
|
||||
- name: './reporter.ts'
|
||||
```
|
||||
|
||||
```
|
||||
[stats] tool_call -> 1
|
||||
[stats] tool_call -> 2
|
||||
[stats] prompt -> 1
|
||||
```
|
||||
|
||||
因为 `ctx.on()` 属于 effect,监听器会随插件一同消失,绝不需要手动维护 `removeListener`。
|
||||
|
||||
## 分发模式
|
||||
|
||||
`emit` 是 5 种分发模式之一。事件采用哪种模式是其契约的一部分,决定了监听器能否返回值、能否并发运行,以及能否彼此短路:
|
||||
|
||||
| 模式 | 调用 | 语义 |
|
||||
|---|---|---|
|
||||
| emit | `ctx.emit(name, ...args)` | 同步广播;不会等待或收集返回的 promise 与值。 |
|
||||
| parallel | `await ctx.parallel(name, ...args)` | 所有监听器并发运行,并一同等待。 |
|
||||
| serial | `await ctx.serial(name, ...args)` | 监听器按顺序运行并等待;第一个非 `null`/`false`/`undefined` 返回值胜出,并停止后续监听器。 |
|
||||
| bail | `ctx.bail(name, ...args)` | serial 的同步版本。 |
|
||||
| waterfall(瀑布式事件) | `ctx.waterfall(name, ...args, next)` | 环绕中间件,见下文。 |
|
||||
|
||||
每个 harness 事件都会在生成的[事件目录](../cordis-catalog/events.md)中记录其模式。
|
||||
|
||||
## Waterfall:转换或短路
|
||||
|
||||
waterfall 是实现拦截的模式。每个监听器都会收到参数和一个 `next()` continuation;它可以转换 `next()` 的返回值,也可以不调用 `next()` 就直接返回,从而短路链条的其余部分。Cordis 文档把后一种行为称为否决。创建 `waterfall-demo.ts`:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Events {
|
||||
'demo/transform'(input: string, next: () => Promise<string>): Promise<string>
|
||||
}
|
||||
}
|
||||
|
||||
export const name = 'waterfall-demo'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// Listener 1: wrap the downstream result.
|
||||
ctx.on('demo/transform', async (input, next) => {
|
||||
const downstream = await next()
|
||||
return downstream.toUpperCase()
|
||||
})
|
||||
|
||||
// Listener 2: short-circuit when it owns the decision.
|
||||
ctx.on('demo/transform', async (input, next) => {
|
||||
if (input.includes('blocked')) return '** blocked **'
|
||||
return next()
|
||||
})
|
||||
|
||||
void (async () => {
|
||||
console.log(await ctx.waterfall('demo/transform', 'hello', async () => 'hello'))
|
||||
console.log(await ctx.waterfall('demo/transform', 'blocked words', async () => 'blocked words'))
|
||||
})()
|
||||
}
|
||||
```
|
||||
|
||||
让 `cordis.yml` 只指向该文件并运行:
|
||||
|
||||
```
|
||||
HELLO
|
||||
** BLOCKED **
|
||||
```
|
||||
|
||||
按顺序看第二行如何产生:监听器 1 先运行并调用 `next()`,从而调用监听器 2;监听器 2 看到 `blocked` 后直接返回而不调用 `next()`,因此最内层默认逻辑(传给 `ctx.waterfall` 的函数)从未运行;返回途中,监听器 1 再把替换消息转换为大写。
|
||||
|
||||
由此得到一项纪律:**只负责观察或标注的 waterfall 监听器必须调用 `next()`**;不调用就直接返回代表有意短路。如果日志监听器忘记调用 `next()`,会悄无声息地吞掉所有下游的默认行为。这一点极其重要,已成为本仓库的常设规则([waterfall 语义](../cordis-primer.md#cordis-waterfall-semantics))。
|
||||
|
||||
harness 使用 waterfall 处理协作插件可以包装或回答的决策:[`agent/request`](../cordis-catalog/events.md#agentrequest--waterfall) 允许插件替换模型调用配置,[`approval/request`](../cordis-catalog/events.md#approvalrequest--waterfall) 允许策略代替用户作答。
|
||||
|
||||
下一章:[配置](05-config.md):来自 `cordis.yml` 的插件选项。
|
||||
|
||||
[](https://github.com/deepseek-harness/deepseek-harness)
|
||||
6
docs/cordis-tutorial/05-config.i18n.yaml
Normal file
6
docs/cordis-tutorial/05-config.i18n.yaml
Normal file
@@ -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
|
||||
05-config.md: fc19add239636fa9e7071d9c77e48595caec1f08
|
||||
05-config.zh.md: 52a75e40672c9a08d285677dd14dcd404b925e5a
|
||||
@@ -1,5 +1,7 @@
|
||||
# 5. Configuration
|
||||
|
||||
English | [中文](05-config.zh.md)
|
||||
|
||||
Each `cordis.yml` entry can carry a `config` block, and the plugin declares a schema that validates it before `apply` runs. Bad config fails the load with a precise error — the plugin never starts half-configured.
|
||||
|
||||
## A configurable plugin
|
||||
|
||||
84
docs/cordis-tutorial/05-config.zh.md
Normal file
84
docs/cordis-tutorial/05-config.zh.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# 5. 配置
|
||||
|
||||
[English](05-config.md) | 中文
|
||||
|
||||
每个 `cordis.yml` 配置项都可以携带 `config` 块,插件则声明一个 schema,在运行 `apply` 前验证该块。错误配置会导致加载失败,并给出准确的错误:插件绝不会在配置不完整时启动。
|
||||
|
||||
## 可配置插件
|
||||
|
||||
创建 `config-demo.ts`,并将其放在 `tmp/cordis-tutorial` 中:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import Schema from 'schemastery'
|
||||
|
||||
export const name = 'config-demo'
|
||||
|
||||
export interface Config {
|
||||
greeting: string
|
||||
targets: string[]
|
||||
}
|
||||
|
||||
export const Config: Schema<Config> = Schema.object({
|
||||
greeting: Schema.string().default('Hello'),
|
||||
targets: Schema.array(String).default(['world']),
|
||||
})
|
||||
|
||||
export function apply(ctx: Context, config: Config) {
|
||||
for (const target of config.targets) {
|
||||
console.log(`${config.greeting}, ${target}!`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
导出的 `Config` 既是 TypeScript 接口,也是同名的运行时 schema:消费方获得类型,Cordis 获得验证器。本仓库使用 [Schemastery](https://github.com/shigma/schemastery) 定义 schema;Cordis 本身接受任意 [Standard Schema](https://standardschema.dev/) 验证器,因此将普通对象导出为 `Config` 无法工作。
|
||||
|
||||
对其进行配置:
|
||||
|
||||
```yaml
|
||||
- name: './config-demo.ts'
|
||||
config:
|
||||
targets: ['alpha', 'beta']
|
||||
```
|
||||
|
||||
运行:
|
||||
|
||||
```
|
||||
Hello, alpha!
|
||||
Hello, beta!
|
||||
```
|
||||
|
||||
未提供 `greeting`,因此 schema 默认值会将其补齐:`apply` 始终会收到完整且经过验证的配置。
|
||||
|
||||
## 明确报错
|
||||
|
||||
现在向它传入无效内容:
|
||||
|
||||
```yaml
|
||||
- name: './config-demo.ts'
|
||||
config:
|
||||
targets: 'not-an-array'
|
||||
```
|
||||
|
||||
```
|
||||
ValidationError: invalid config:
|
||||
- $.targets expected array but got not-an-array (at targets)
|
||||
```
|
||||
|
||||
插件的 fiber 进入 FAILED 状态,本教程的启动器打印错误后以状态码 1 退出。如果某个插件的 schema 有效配置命名了不可用的资源或提供方,该插件也应当在能解析该引用时立即拒绝。
|
||||
|
||||
## 计算得到的配置值
|
||||
|
||||
本仓库使用的 loader 支持 `!!js` 标签,用于必须在加载时计算的配置值,例如从环境中读取 API key:
|
||||
|
||||
```yaml
|
||||
- name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
```
|
||||
|
||||
`!!js` **仅在 `config` 内有效**。配置项元数据(`name`、`id`、`disabled`、`inject` 等)是静态的;`disabled: !!js ...` 会生成一个真值表达式对象,始终禁用该配置项。详见 [loader 配置](../cordis-primer.md#loader-configuration)。
|
||||
|
||||
下一章:[组合与 HMR](06-composition-and-hmr.md):将 `cordis.yml` 视为应用。
|
||||
|
||||
[](https://github.com/deepseek-harness/deepseek-harness)
|
||||
6
docs/cordis-tutorial/06-composition-and-hmr.i18n.yaml
Normal file
6
docs/cordis-tutorial/06-composition-and-hmr.i18n.yaml
Normal file
@@ -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
|
||||
06-composition-and-hmr.md: 66d6a9d93fe39baa881940ba32388979e2678505
|
||||
06-composition-and-hmr.zh.md: ebe63fc26607ae6d9344c4795a7975496ed901b5
|
||||
@@ -1,5 +1,7 @@
|
||||
# 6. Composition and HMR
|
||||
|
||||
English | [中文](06-composition-and-hmr.zh.md)
|
||||
|
||||
Every capability built so far is a plugin, and `cordis.yml` selects the application's plugin tree. This chapter changes that composition, hot-reloads a plugin, and diagnoses a plugin that never loads.
|
||||
|
||||
## Entries are more than a name
|
||||
|
||||
113
docs/cordis-tutorial/06-composition-and-hmr.zh.md
Normal file
113
docs/cordis-tutorial/06-composition-and-hmr.zh.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# 6. 组合与 HMR(热模块替换)
|
||||
|
||||
[English](06-composition-and-hmr.md) | 中文
|
||||
|
||||
到目前为止构建的每项功能都是插件,`cordis.yml` 则选择应用的插件树。本章会改变这种组合、热重载一个插件,并诊断始终无法加载的插件。
|
||||
|
||||
## 配置项不只有名称
|
||||
|
||||
配置项除了 `name` 和 `config`,还接受其他元数据:
|
||||
|
||||
```yaml
|
||||
- id: greeter # stable identity for this entry
|
||||
name: './greeter.ts'
|
||||
- id: consumer
|
||||
name: './consumer.ts'
|
||||
disabled: true # keep the entry, skip mounting it
|
||||
```
|
||||
|
||||
`id` 为配置项提供稳定标识,使 loader 能区分修改现有配置项与先删除再添加。`disabled: true` 会卸载插件而不删除其配置项;改回原值后,插件以及所有因依赖其服务而处于 PENDING 的插件都会再次加载。
|
||||
|
||||
组可以嵌套一份配置项子列表,并将其作为一个单元加载和卸载;`isolate` 则为一个组提供某项服务名称的独立实例,因此两个组可以各自看到配置不同的 `bash`,互不影响。这些概念值得在用到之前先了解;[Cordis 入门](../cordis-primer.md)和[服务隔离示例](../user/develop/framework/service.md#service-isolation)介绍了详细内容。
|
||||
|
||||
## 热模块替换
|
||||
|
||||
卸载会释放 effect([第 2 章](02-lifecycle-and-effects.md)),加载则遵循依赖关系([第 3 章](03-services.md)),因此 HMR 可以先卸载、再加载,以替换正在运行的插件。`@cordisjs/plugin-hmr` 插件会监视文件,并在保存时执行这一过程。
|
||||
|
||||
在 `tmp/cordis-tutorial` 中编写 `cordis.yml`:
|
||||
|
||||
```yaml
|
||||
- id: logger
|
||||
name: '@cordisjs/plugin-logger-console'
|
||||
- id: timer
|
||||
name: '@cordisjs/plugin-timer'
|
||||
- id: hmr
|
||||
name: '@cordisjs/plugin-hmr'
|
||||
config:
|
||||
root: ['.']
|
||||
- id: hello
|
||||
name: './hello.ts'
|
||||
```
|
||||
|
||||
列表中增加了两个支持插件:HMR 通过 Cordis logger 服务记录日志,因此没有 console exporter 时看不到其消息;它还会 `inject` `timer` 服务来实现去抖,如果没有 `@cordisjs/plugin-timer`,它就会永远停在 PENDING,而且不发出任何提示。下一节就讨论这种静默状态。
|
||||
|
||||
HMR 通过 Loader 的原生辅助工具读取 Node 的 loader 内部结构。请在 tsx 下运行 Cordis:
|
||||
|
||||
```sh
|
||||
node --import tsx ../../vendor/cordis/bin.js
|
||||
```
|
||||
|
||||
现在编辑 `hello.ts`,修改日志消息并保存:
|
||||
|
||||
```
|
||||
hello from my first plugin
|
||||
2026-07-22 15:44:36 [I] hmr watching [ '.' ]
|
||||
2026-07-22 15:44:39 [I] hmr reload plugin at hello.ts
|
||||
hello from my EDITED plugin
|
||||
```
|
||||
|
||||
旧实例先卸载(其所有 effect 都会回卷),新代码随后加载,`apply` 再次运行。按 Ctrl-C 停止进程。编辑 `cordis.yml` 本身也会触发更新:loader 按 `id` 比较配置项,只挂载、卸载或重新配置发生变化的部分。这就是上述配置项显式携带 `id` 的原因:不带该字段的配置项在每次读取时都会获得一个新生成的 id,所以只要配置文件发生任何编辑,即使自身文本未变,它也会被视为先删除再添加并重新挂载。
|
||||
|
||||
## 诊断始终无法加载的插件
|
||||
|
||||
依赖驱动加载也有另一面:如果插件的 `inject` 指定了无人提供的服务,它就会一直等待,不输出任何内容。这不是错误,因为 PENDING 是合法状态,提供方可能稍后才挂载。
|
||||
|
||||
你可以直接查看这些状态。每个上下文都能枚举插件注册表;创建 `diagnose.ts`:
|
||||
|
||||
```ts
|
||||
import { FiberState, type Context } from 'cordis'
|
||||
|
||||
export const name = 'diagnose'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
setTimeout(() => {
|
||||
for (const runtime of ctx.registry.values()) {
|
||||
for (const fiber of runtime.fibers) {
|
||||
if (fiber.state === FiberState.PENDING) {
|
||||
console.log(`${fiber.name} is PENDING — a required service is missing`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
```
|
||||
|
||||
再创建一个依赖无法满足的插件 `needs-timer.ts`:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
export const name = 'needs-timer'
|
||||
export const inject = ['timer']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
console.log('needs-timer loaded')
|
||||
}
|
||||
```
|
||||
|
||||
```yaml
|
||||
- name: './needs-timer.ts'
|
||||
- name: './diagnose.ts'
|
||||
```
|
||||
|
||||
运行它(直接执行 `node --import tsx ../../vendor/cordis/bin.js`,按 Ctrl-C 停止):
|
||||
|
||||
```
|
||||
needs-timer is PENDING — a required service is missing
|
||||
```
|
||||
|
||||
`inject: ['timer']` 没有提供方。向列表添加 `- name: '@cordisjs/plugin-timer'` 后,插件就会加载。如果插件既不执行任何操作,也不报告任何内容,请检查其 fiber 状态。不加 PENDING 过滤条件进行迭代时,还会看到 loader 自身的插件(Loader、Include)处于 ACTIVE,因为配置文件本身也是通过插件挂载的。
|
||||
|
||||
下一章:[进入 harness](07-into-the-harness.md):把相同模式用于真实的 harness 服务。
|
||||
|
||||
[](https://github.com/deepseek-harness/deepseek-harness)
|
||||
6
docs/cordis-tutorial/07-into-the-harness.i18n.yaml
Normal file
6
docs/cordis-tutorial/07-into-the-harness.i18n.yaml
Normal file
@@ -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
|
||||
07-into-the-harness.md: 6ec42c50fe5059955734fe7bc46117538dafaffc
|
||||
07-into-the-harness.zh.md: 32b21b008837e2972a53db9d893788dc6a7de9a9
|
||||
@@ -1,5 +1,7 @@
|
||||
# 7. Into the harness
|
||||
|
||||
English | [中文](07-into-the-harness.zh.md)
|
||||
|
||||
This chapter registers a model-callable tool with the harness's `tools` service, executes it through the harness tool pipeline, and observes the result event. It remains keyless and does not call a model.
|
||||
|
||||
## A tool plugin
|
||||
|
||||
107
docs/cordis-tutorial/07-into-the-harness.zh.md
Normal file
107
docs/cordis-tutorial/07-into-the-harness.zh.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# 7. 进入 harness
|
||||
|
||||
[English](07-into-the-harness.md) | 中文
|
||||
|
||||
本章会向 harness 的 `tools` 服务注册一个可由模型调用的工具,通过 harness 工具流水线执行它,并观察结果事件。整个示例无需密钥,也不会调用模型。
|
||||
|
||||
## 工具插件
|
||||
|
||||
创建 `greet-tool.ts`,将它放在 `tmp/cordis-tutorial` 中:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
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, description: 'Who to greet' },
|
||||
},
|
||||
output: {
|
||||
schema: { type: 'string' },
|
||||
render: (_args, value) => [{ type: 'text', text: value }],
|
||||
},
|
||||
async execute(args) {
|
||||
return `Hello, ${args.name}!`
|
||||
},
|
||||
}))
|
||||
|
||||
// Drive one call through the real execution pipeline, standing in for
|
||||
// the model. CallId brands the correlation id a provider would issue.
|
||||
void (async () => {
|
||||
const result = await ctx.tools.execute({
|
||||
callId: CallId('demo-1'),
|
||||
name: 'greet',
|
||||
arguments: { name: 'Cordis' },
|
||||
signal: new AbortController().signal,
|
||||
})
|
||||
console.log('tool replied:', JSON.stringify(result.content))
|
||||
})()
|
||||
}
|
||||
```
|
||||
|
||||
这里的每个模式都来自前几章:`inject: ['tools']`([第 3 章](03-services.md))会让插件等待工具注册表就绪;`ctx.tools.register(...)` 会把注册 disposer 附着到插件([第 2 章](02-lifecycle-and-effects.md)),因此卸载时会注销工具。`defineTool` 将 `parameters` 规约转换为向模型展示的 JSON Schema,推导 `args` 的类型,并在 `execute` 运行前校验模型提供的参数。工具返回由 `output.schema` 声明的规范值;`output.render` 则另行生成原生且持久的结果内容。
|
||||
|
||||
## 观察插件
|
||||
|
||||
创建 `tool-logger.ts`。这是一个独立插件,通过 harness 的 `tools/result` 事件观察应用中的每次工具调用:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const name = 'tool-logger'
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.on('tools/result', (exec, result) => {
|
||||
const text = result.content
|
||||
.map(block => (block.type === 'text' ? block.text : ''))
|
||||
.join('')
|
||||
console.log(`[tool-logger] ${exec.name} -> ${text}`)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
`import type {} from '@deepseek-ai/dsh-tools'` 行会引入该包的声明合并,使 `'tools/result'` 及其 payload 具有类型。这与第 4 章导入 `stats.ts` 的做法相同,只是扩展到了包级别。
|
||||
|
||||
## 组合并运行
|
||||
|
||||
```yaml
|
||||
- name: '@deepseek-ai/dsh-system-prompt'
|
||||
- name: '@deepseek-ai/dsh-tools'
|
||||
- name: './tool-logger.ts'
|
||||
- name: './greet-tool.ts'
|
||||
```
|
||||
|
||||
`@deepseek-ai/dsh-tools` 会注入 `systemPrompt` 服务,因为工具需要向系统提示词贡献 schema,所以组合中也要列出该服务的提供方。缺少提供方时,工具插件会像[第 6 章](06-composition-and-hmr.md)所述那样保持 PENDING。
|
||||
|
||||
```sh
|
||||
node --import tsx ../../vendor/cordis/bin.js
|
||||
```
|
||||
|
||||
```
|
||||
[tool-logger] greet -> Hello, Cordis!
|
||||
tool replied: [{"type":"text","text":"Hello, Cordis!"}]
|
||||
```
|
||||
|
||||
logger 会先触发:`tools/result` 在结果物化过程中发出,早于 `execute` 的 promise 向调用方返回结果。两个插件都不知道另一个插件存在,它们由注册表服务和事件连接。
|
||||
|
||||
## 从这里走向完整 agent(智能体)
|
||||
|
||||
真实 agent 就是这套组合再加上更多插件:LLM(大语言模型)适配器、agent loop(智能体循环)、持久化和前端。对照 [examples/headless-agent/cordis.yml](../../examples/headless-agent/cordis.yml),你现在已经可以读懂其中每个配置项。将 `greet-tool.ts` 加入该文件的副本即可。
|
||||
|
||||
后续可以阅读:
|
||||
|
||||
- [构建工具](../user/develop/basic/tool.md):深入了解 `defineTool`,包括呈现和更丰富的 schema。
|
||||
- [三层功能设计](../user/develop/practice/index.md):harness 如何组织可替换功能。
|
||||
- 生成的[服务](../cordis-catalog/services.md)与[事件](../cordis-catalog/events.md)目录:可以注入和监听的所有内容。
|
||||
- [架构](../architecture.md):这些插件所处的系统地图。
|
||||
|
||||
[](https://github.com/deepseek-harness/deepseek-harness)
|
||||
6
docs/cordis-tutorial/index.i18n.yaml
Normal file
6
docs/cordis-tutorial/index.i18n.yaml
Normal file
@@ -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: af622ad4e35829c6283c40f1b0019d7959dac973
|
||||
index.zh.md: 35bad552ecce9c0496b0ed88b041a8109c81945b
|
||||
@@ -1,5 +1,7 @@
|
||||
# Cordis tutorial
|
||||
|
||||
English | [中文](index.zh.md)
|
||||
|
||||
Cordis is the plugin framework underneath the DeepSeek Harness SDK: a small runtime where every capability — tools, LLM adapters, file access, the agent loop itself — is a plugin mounted into a shared context. This tutorial teaches Cordis hands-on: each chapter is a runnable example you build in a scratch directory inside this repository, ending with a plugin wired into real harness services.
|
||||
|
||||
The audience is agent developers. You do not need deep TypeScript experience; the [TypeScript notes](#typescript-notes) below explain the syntax that may be unfamiliar, and every chapter shows the exact commands and expected output.
|
||||
@@ -41,6 +43,8 @@ That one-file launcher (see [vendor/cordis/bin.js](../../vendor/cordis/bin.js))
|
||||
6. [Composition and HMR](06-composition-and-hmr.md) — the config file as a plugin tree, hot reload, and diagnosing a plugin that never loads.
|
||||
7. [Into the harness](07-into-the-harness.md) — register a model-callable tool against real harness services.
|
||||
|
||||
<a id="typescript-notes"></a>
|
||||
|
||||
## TypeScript notes
|
||||
|
||||
The examples use three TypeScript features beyond ordinary modern JavaScript:
|
||||
|
||||
58
docs/cordis-tutorial/index.zh.md
Normal file
58
docs/cordis-tutorial/index.zh.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Cordis 教程
|
||||
|
||||
[English](index.md) | 中文
|
||||
|
||||
Cordis 是 DeepSeek Harness SDK 底层的插件框架:它是一个小型运行时,其中的每项能力,包括工具、LLM(大语言模型)适配器、文件访问乃至 agent loop(智能体循环)本身,都是挂载到共享上下文中的插件。本教程通过动手实践讲解 Cordis:每一章都是一个可以运行的示例,你将在本仓库内的临时目录中逐步构建它,最后把一个插件接入真实的 harness 服务。
|
||||
|
||||
本教程面向 agent 开发者。你不需要深入掌握 TypeScript;下文的 [TypeScript 说明](#typescript-notes)会解释可能陌生的语法,并且每一章都会给出确切命令和预期输出。
|
||||
|
||||
如果你想阅读精简的概念参考,而不是逐步实践,请参阅 [Cordis 入门](../cordis-primer.md)。详尽的 API 参考见生成的[事件](../cordis-catalog/events.md)与[服务](../cordis-catalog/services.md)目录,以及 [Cordis 核心 API](../cordis-catalog/core/context.md)页面。
|
||||
|
||||
## 准备工作
|
||||
|
||||
你需要克隆本仓库并安装依赖,具体前置条件见[快速入门](../user/guide/quickstart.md)。本教程不需要 API 密钥;所有示例均可在无密钥环境中运行。
|
||||
|
||||
```sh
|
||||
git clone https://github.com/deepseek-harness/deepseek-harness.git
|
||||
cd deepseek-harness
|
||||
pnpm install
|
||||
```
|
||||
|
||||
创建各章使用的临时目录。`tmp/` 已被 git 忽略,因此你在其中写入的任何内容都不会进入版本控制:
|
||||
|
||||
```sh
|
||||
mkdir -p tmp/cordis-tutorial
|
||||
cd tmp/cordis-tutorial
|
||||
```
|
||||
|
||||
每一章都从该目录运行同一条命令:
|
||||
|
||||
```sh
|
||||
node --import tsx ../../vendor/cordis/bin.js
|
||||
```
|
||||
|
||||
这个单文件启动器(见 [vendor/cordis/bin.js](../../vendor/cordis/bin.js))会创建根 `Context`、挂载 Loader 插件,并让它从当前目录加载 `./cordis.yml`。其余所有内容,包括有哪些插件以及如何配置它们,都来自你稍后将编写的 YAML 文件。`--import tsx` 标志让 Node 无需构建步骤即可运行配置所指向的 TypeScript 文件。
|
||||
|
||||
## 章节
|
||||
|
||||
1. [你的第一个插件](01-first-plugin.md):插件是函数,由 loader 挂载。
|
||||
2. [生命周期与 effect](02-lifecycle-and-effects.md):由 Cordis 管理的注册会在所属插件卸载时撤销。
|
||||
3. [服务](03-services.md):在 `ctx` 上公开一项能力,并通过 `inject` 依赖它。
|
||||
4. [事件](04-events.md):类型化事件、广播分发和 waterfall(瀑布式事件)的短路行为。
|
||||
5. [配置](05-config.md):读取 `cordis.yml` 中经过校验的配置,并在输入错误时快速失败。
|
||||
6. [组合与 HMR(热模块替换)](06-composition-and-hmr.md):把配置文件作为插件树,使用热重载,并诊断始终无法加载的插件。
|
||||
7. [进入 harness](07-into-the-harness.md):基于真实的 harness 服务注册一个可由模型调用的工具。
|
||||
|
||||
<a id="typescript-notes"></a>
|
||||
|
||||
## TypeScript 说明
|
||||
|
||||
这些示例使用了普通现代 JavaScript 之外的三项 TypeScript 功能:
|
||||
|
||||
- **类型注解** 描述值,但不会改变运行时行为:`ctx: Context` 表示 `ctx` 具备 Cordis 上下文 API,`who: string` 接受文本,而 `string[]` 表示字符串数组。
|
||||
- **`import type { Context } from 'cordis'`** 只导入类型信息。它在运行时会消失,因此仅为类型注解使用 `Context` 的插件文件不会增加运行时依赖。
|
||||
- **声明合并**(`declare module 'cordis' { ... }`)会为 Cordis 已经声明的接口添加你的条目,例如新 `ctx.greeter` 属性的类型或事件名称。它不会生成任何运行时接线;插件必须另行提供服务或发出事件。第 3 章会完整展示该模式。
|
||||
|
||||
第 5 章还会使用 `interface` 描述配置对象的字段,并使用 `Schema<Config>` 这类泛型表示 schema 所校验的对象形状。你可以直接照写这些声明;周围的正文会解释每项声明连接了什么。
|
||||
|
||||
[](https://github.com/deepseek-harness/deepseek-harness)
|
||||
6
docs/core-data-structures/commands.i18n.yaml
Normal file
6
docs/core-data-structures/commands.i18n.yaml
Normal file
@@ -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
|
||||
commands.md: 056c775f4c2e1586447db11821e5c7d56be01881
|
||||
commands.zh.md: 1a51305df356d8becf8c5517704dc375cdb8b585
|
||||
@@ -1,5 +1,7 @@
|
||||
# Human Commands
|
||||
|
||||
English | [中文](commands.zh.md)
|
||||
|
||||
The human-command seam of [`dsh-commands`](../../packages/ui/commands). Interactive adapters use it to discover and directly execute plugin-owned commands for an exact agent without creating a model message. The [command Agent Note](../../.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.md) owns dispatch and lifecycle rationale; the [package README](../../packages/ui/commands/README.md) owns composition and limitations.
|
||||
|
||||
Source: [`packages/ui/commands/src/index.ts`](../../packages/ui/commands/src/index.ts)
|
||||
|
||||
86
docs/core-data-structures/commands.zh.md
Normal file
86
docs/core-data-structures/commands.zh.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# 用户命令
|
||||
|
||||
[English](commands.md) | 中文
|
||||
|
||||
[`dsh-commands`](../../packages/ui/commands) 的用户命令 seam。交互式适配器用它发现插件拥有的命令,并针对确切的 agent(智能体)直接执行这些命令,而不创建模型消息。[命令 Agent Note(agent 决策记录)](../../.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.md) 负责分发与生命周期的决策依据;[包(package)README](../../packages/ui/commands/README.md) 负责组合方式与限制。
|
||||
|
||||
来源:[`packages/ui/commands/src/index.ts`](../../packages/ui/commands/src/index.ts)
|
||||
|
||||
## 输入元数据
|
||||
|
||||
该 seam 公开一个可选的非结构化输入提示。命令的可用性由插件组合决定:每个消费注册表的适配器都会看到全部生效定义。
|
||||
|
||||
```ts type-equiv
|
||||
/** Immutable metadata for a command's optional unstructured input. */
|
||||
interface CommandInputDescriptor {
|
||||
/** Placeholder shown before the user supplies free-form input. */
|
||||
readonly hint: string
|
||||
}
|
||||
```
|
||||
|
||||
## 定义
|
||||
|
||||
`CommandDefinition` 是由插件编写的注册定义。注册表会验证并冻结一份与原始注册对象脱离的生效定义。
|
||||
|
||||
```ts type-equiv
|
||||
/** Plugin-owned command registration. */
|
||||
interface CommandDefinition {
|
||||
/** Lowercase command name without the leading slash. */
|
||||
readonly name: string
|
||||
/** Human-readable summary used in discovery UI. */
|
||||
readonly description: string
|
||||
/** Optional free-form input hint advertised to capable clients. */
|
||||
readonly input?: CommandInputDescriptor
|
||||
/** Execute against the receiving agent without sending the command to the model. */
|
||||
readonly handler: (invocation: CommandInvocation) => CommandResult | Promise<CommandResult>
|
||||
}
|
||||
```
|
||||
|
||||
## 调用与结果
|
||||
|
||||
适配器拥有取消操作,并传入确切的目标 agent。`rawInput` 紧接在解析后的名称之后,并保留适配器传入的分隔符与后缀。结果会直接呈现给 UI,而不是工具结果或会话事件。
|
||||
|
||||
```ts type-equiv
|
||||
/** Invocation passed to one registered command handler. */
|
||||
interface CommandInvocation {
|
||||
/** Exact agent whose human-facing surface received the command. */
|
||||
readonly agent: Agent
|
||||
/** Exact text following the registered command name, including separator whitespace. */
|
||||
readonly rawInput: string
|
||||
/** Cancellation signal owned by the dispatching UI request. */
|
||||
readonly signal: AbortSignal
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Expected command outcome rendered directly by the dispatching UI. */
|
||||
type CommandResult =
|
||||
| { readonly kind: 'success'; readonly text?: string }
|
||||
| { readonly kind: 'error'; readonly text: string }
|
||||
```
|
||||
|
||||
## 发现与解析视图
|
||||
|
||||
作用域解析后,适配器会获得不含处理器的不可变描述符。`parseCommand()` 在注册表解析前返回 `ParsedCommand`;语法有效的输入仍可能指向不可用的命令。
|
||||
|
||||
```ts type-equiv
|
||||
/** Handler-free immutable command view returned to UI adapters. */
|
||||
interface CommandDescriptor {
|
||||
/** Lowercase command name without the leading slash. */
|
||||
readonly name: string
|
||||
/** Human-readable summary used in discovery UI. */
|
||||
readonly description: string
|
||||
/** Optional free-form input hint advertised to capable clients. */
|
||||
readonly input?: CommandInputDescriptor
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Syntactically valid slash command before registry resolution. */
|
||||
interface ParsedCommand {
|
||||
/** Lowercase command name without the leading slash. */
|
||||
readonly name: string
|
||||
/** Exact text following the command name. */
|
||||
readonly rawInput: string
|
||||
}
|
||||
```
|
||||
6
docs/core-data-structures/goal.i18n.yaml
Normal file
6
docs/core-data-structures/goal.i18n.yaml
Normal file
@@ -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
|
||||
goal.md: 2e8d296eeda6e5f69c0f92829e347b7f55f41fa9
|
||||
goal.zh.md: a9c946e7cd37cf948c7ac0f3e4d0ea35ac80d614
|
||||
@@ -1,5 +1,7 @@
|
||||
# Same-session goals
|
||||
|
||||
English | [中文](goal.zh.md)
|
||||
|
||||
Types shared by the event-sourced goal domain and its policy consumers. The [goal-domain Agent Note](../../.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md) owns the persistence and activation decisions; this page records the literal shapes from [`packages/goal/goal/src/types.ts`](../../packages/goal/goal/src/types.ts).
|
||||
|
||||
## Identity and lifecycle
|
||||
|
||||
145
docs/core-data-structures/goal.zh.md
Normal file
145
docs/core-data-structures/goal.zh.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# 同会话目标
|
||||
|
||||
[English](goal.md) | 中文
|
||||
|
||||
事件溯源目标领域及其策略消费方共享的类型。[目标领域 Agent Note(agent 决策记录)](../../.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md)负责记录持久化与激活决策;本页记录 [`packages/goal/goal/src/types.ts`](../../packages/goal/goal/src/types.ts) 中的字面形态。
|
||||
|
||||
## 标识与生命周期
|
||||
|
||||
`GoalId` 是[品牌化 id](core.md#branded-ids)。调用方通过 `GoalRef` 修改一个确切修订版本;每次获准的持久变更都会递增修订号。
|
||||
|
||||
```ts type-equiv
|
||||
/** Compare-and-set identity for one exact goal revision. */
|
||||
interface GoalRef {
|
||||
/** Stable goal identity. */
|
||||
readonly id: GoalId
|
||||
/** Positive revision; every durable mutation increments it. */
|
||||
readonly revision: number
|
||||
}
|
||||
```
|
||||
|
||||
持久阶段回答目标发生了什么。进程本地激活状态则另行回答续跑消费方能否开始另一个 Round。
|
||||
|
||||
```ts type-equiv
|
||||
/** Durable continuation phase. Activation is process-local and separate. */
|
||||
type GoalPhase =
|
||||
| 'active'
|
||||
| 'paused'
|
||||
| 'blocked'
|
||||
| 'complete'
|
||||
```
|
||||
|
||||
阻塞是唯一表示「因问题而停止」的持久状态。由策略负责的阻塞原因会携带一个用于路由、稳定且采用 lower-kebab-case 的代码,以及一段供人和模型阅读的自由文本说明。
|
||||
|
||||
```ts type-equiv
|
||||
/** Machine-routable and human-readable explanation for a blocked goal. */
|
||||
interface GoalBlockReason {
|
||||
/** Stable lower-kebab-case classification chosen by the blocking policy. */
|
||||
readonly code: string
|
||||
/** Non-empty explanation shown to humans and models. */
|
||||
readonly message: string
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Full durable state written by every non-clear goal mutation. */
|
||||
interface GoalSnapshot extends GoalRef {
|
||||
/** Human-requested completion objective. */
|
||||
readonly objective: string
|
||||
/** Durable lifecycle phase. */
|
||||
readonly phase: GoalPhase
|
||||
/** Present exactly while `phase` is `blocked`. */
|
||||
readonly blockedReason?: GoalBlockReason
|
||||
/** Total admitted goal-round cap. */
|
||||
readonly maxGoalRounds: number
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Current goal projection, including values derived from the session log. */
|
||||
interface GoalView extends GoalSnapshot {
|
||||
/** Highest admitted round number for this goal. */
|
||||
readonly roundsStarted: number
|
||||
/** Epoch milliseconds of the create mutation. */
|
||||
readonly createdAt: number
|
||||
/** Epoch milliseconds of the latest mutation. */
|
||||
readonly updatedAt: number
|
||||
/** Process-local continuation eligibility; never persisted. */
|
||||
readonly activation: GoalActivation
|
||||
}
|
||||
```
|
||||
|
||||
## 持久变更
|
||||
|
||||
每次变更都是 Round 编号为 0、来源为目标的 `user/message`,其元数据要么是完整快照,要么是清除墓碑。版本、元数据、目标来源和逐字渲染内容共同构成一项回放不变量。
|
||||
|
||||
```ts type-equiv
|
||||
/** Full-snapshot goal mutation retained in a model-visible context event. */
|
||||
interface GoalSnapshotChangeMeta {
|
||||
readonly kind: 'goal/change'
|
||||
readonly version: 1
|
||||
readonly operation: Exclude<GoalOperation, 'clear'>
|
||||
readonly goal: GoalSnapshot
|
||||
readonly roundsStarted: number
|
||||
readonly createdAt: number
|
||||
readonly updatedAt: number
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Tombstone retained when the current goal is cleared. */
|
||||
interface GoalClearChangeMeta {
|
||||
readonly kind: 'goal/change'
|
||||
readonly version: 1
|
||||
readonly operation: 'clear'
|
||||
readonly cleared: GoalRef
|
||||
readonly clearedAt: number
|
||||
}
|
||||
```
|
||||
|
||||
目标状态变更使用 Round `0`。续跑消费方会为每个获准的用户消息轮次标注正数且连续的 Round 编号和当前修订号;回放会拒绝编号缺口、陈旧修订号、已停止阶段和超出上限。
|
||||
|
||||
```ts type-equiv
|
||||
/** Message attribution for durable goal state and continuation rounds. */
|
||||
interface GoalMessageSource {
|
||||
readonly kind: 'goal'
|
||||
readonly goalId: GoalId
|
||||
readonly revision: number
|
||||
/** Zero for state changes; positive for admitted continuation rounds. */
|
||||
readonly round: number
|
||||
}
|
||||
```
|
||||
|
||||
## 请求与通知
|
||||
|
||||
创建操作会区分调用方省略的值与部署选择,`create()` 会在内部解析后者。编辑是局部替换,其运行时校验器要求至少提供一个字段。每条变更通知都会携带获准的操作和确切修订号;清除操作不带 `goal`。
|
||||
|
||||
```ts type-equiv
|
||||
/** Input whose omitted round cap is resolved by the service configuration. */
|
||||
interface CreateGoalRequest {
|
||||
readonly objective: string
|
||||
readonly maxGoalRounds?: number
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Fields changed by an edit; at least one must be present. */
|
||||
interface EditGoalRequest {
|
||||
readonly objective?: string
|
||||
readonly maxGoalRounds?: number
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Live notification after one goal mutation has been accepted for logging. */
|
||||
interface GoalChanged {
|
||||
readonly operation: GoalOperation
|
||||
readonly ref: GoalRef
|
||||
/** Absent for a clear tombstone. */
|
||||
readonly goal?: GoalView
|
||||
}
|
||||
```
|
||||
|
||||
## 服务行为
|
||||
|
||||
[`GoalService`](../../packages/goal/goal/src/index.ts) 解析创建默认值、执行严格回放折叠、校验确切的活跃 agent 身份、以比较并设置方式执行变更、叠加延迟注入,并发出 `goal/changed` 通知;监听器故障会被隔离。包 [README](../../packages/goal/goal/README.md) 负责记录可调用契约和面向模型的契约。
|
||||
6
docs/core-data-structures/lsp.i18n.yaml
Normal file
6
docs/core-data-structures/lsp.i18n.yaml
Normal file
@@ -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
|
||||
lsp.md: 62b133cbfdf521e067c56355664d7514a613397f
|
||||
lsp.zh.md: d7000970ec9114bcdad40a39d2712d48b9865529
|
||||
@@ -1,5 +1,7 @@
|
||||
# LSP navigation
|
||||
|
||||
English | [中文](lsp.zh.md)
|
||||
|
||||
The LSP seam — a [capability seam](../../.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md) exposing semantic code navigation on one `ctx.lsp` service, split across packages: interface ([dsh-lsp](../../packages/lsp/lsp), `ctx.lsp` + the provider registry), a generic implementation ([dsh-lsp-local](../../packages/lsp/lsp-local), a configured stdio language-server host), and consumer ([dsh-tool-lsp](../../packages/lsp/tool-lsp), the `lsp` tool schema). LSP is **one optional capability**, not part of the agent-loop spine — so its vocabulary lives here, not in [core.md](core.md). A provider swap does not change how the model asks for navigation.
|
||||
|
||||
Source: [`packages/lsp/lsp/src/types.ts`](../../packages/lsp/lsp/src/types.ts)
|
||||
|
||||
165
docs/core-data-structures/lsp.zh.md
Normal file
165
docs/core-data-structures/lsp.zh.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# LSP 导航
|
||||
|
||||
[English](lsp.md) | 中文
|
||||
|
||||
LSP seam 是一个[能力 seam](../../.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md):它在单一 `ctx.lsp` 服务上公开语义代码导航,并拆分到多个包(package):接口([dsh-lsp](../../packages/lsp/lsp),`ctx.lsp` + 提供方注册表)、通用实现([dsh-lsp-local](../../packages/lsp/lsp-local),经过配置的 stdio 语言服务器宿主)和消费方([dsh-tool-lsp](../../packages/lsp/tool-lsp),即 `lsp` 工具 schema)。LSP 是**一项可选能力**,不属于 agent loop(智能体循环)主干,因此其词汇定义在此而非 [core.md](core.md) 中。更换提供方不会改变模型请求导航的方式。
|
||||
|
||||
源文件:[`packages/lsp/lsp/src/types.ts`](../../packages/lsp/lsp/src/types.ts)
|
||||
|
||||
## 操作与坐标
|
||||
|
||||
seam 与模型恰好公开 4 项语义查询;该联合是闭合的,因此新增一项查询会通过编译强制要求同步修改 seam、提供方和工具。位置与范围采用从零开始的 UTF-16 坐标,与协议一致;面向模型的工具采用从 1 开始的光标约定,并在输入和输出时进行转换。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* The four semantic queries the seam and model expose. A closed union: adding an operation is a
|
||||
* compile-enforced change across the seam, providers, and the tool. Symbols and call hierarchy are
|
||||
* deliberately deferred (they need different schemas).
|
||||
*/
|
||||
type LspOperation = 'goToDefinition' | 'findReferences' | 'goToImplementation' | 'hover'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** A zero-based UTF-16 cursor coordinate, matching the LSP wire convention. */
|
||||
interface LspPosition {
|
||||
/** Zero-based line. */
|
||||
readonly line: number
|
||||
/** Zero-based UTF-16 code-unit offset within the line. */
|
||||
readonly character: number
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** A zero-based UTF-16 half-open range `[start, end)`. */
|
||||
interface LspRange {
|
||||
readonly start: LspPosition
|
||||
readonly end: LspPosition
|
||||
}
|
||||
```
|
||||
|
||||
## 请求
|
||||
|
||||
每个字段都是必填项:`workspaceRoot` 由调用方提供,`languageId` 来自提供方注册而非请求,超时与结果上限由消费方决定。因此没有字段需要由实现提供默认值,也不存在 `resolve()` 步骤。提供方收到调用方请求和派生的 `languageId`;后者只用于同步瞬态文档,从不参与选择。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* A caller's normalized query. Every field is required: `workspaceRoot` is caller-supplied,
|
||||
* `languageId` comes from the provider registration (not here), and consumers own timeouts and
|
||||
* result limits — so no field needs implementation defaulting and there is no `resolve()` step.
|
||||
*/
|
||||
interface LspQueryRequest {
|
||||
/** Which semantic query to run. */
|
||||
readonly operation: LspOperation
|
||||
/** The source file to query (relative to `workspaceRoot` or absolute; the provider canonicalizes). */
|
||||
readonly filePath: string
|
||||
/** The zero-based UTF-16 cursor position to query at. */
|
||||
readonly position: LspPosition
|
||||
/** The workspace root the provider resolves against and indexes; required, never defaulted. */
|
||||
readonly workspaceRoot: string
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* A request as a provider receives it: the caller's {@link LspQueryRequest} plus the `languageId`
|
||||
* the seam derived from the provider's extension mapping. The language id only synchronizes the
|
||||
* transient document; it does not participate in selection.
|
||||
*/
|
||||
interface LspProviderQuery extends LspQueryRequest {
|
||||
/** The LSP language id for `filePath`, from this provider's extension mapping. */
|
||||
readonly languageId: string
|
||||
}
|
||||
```
|
||||
|
||||
## 结果
|
||||
|
||||
这是一个闭合的可辨识联合:导航操作规范化为 `locations`,`hover` 规范化为内容或 `null`。消费方使用 `switch` 对 `kind` 做穷尽处理,因此新增分支会使编译失败,直到完成处理。`findReferences` 始终包含声明;提供方在内部强制保证这一点,因此调用方没有对应 flag。`locations` 变体携带 `resolvedWorkspaceRoot`,即提供方对请求中 `workspaceRoot` 的规范形式,也是其 `file:` URI 所相对的根目录;调用方在相对化显示路径时应使用它,而不是可能经过符号链接的请求根目录。
|
||||
|
||||
```ts type-equiv
|
||||
/** One resolved location: a document URI and the range within it. */
|
||||
interface LspLocation {
|
||||
/** The target document URI (`file:` or otherwise), verbatim from the server. */
|
||||
readonly uri: string
|
||||
/** The range within the target document. */
|
||||
readonly range: LspRange
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Normalized hover content, or `null` for no hover at the position. */
|
||||
interface LspHover {
|
||||
/** The normalized hover text (markdown or plaintext, provider-joined). */
|
||||
readonly contents: string
|
||||
/** The range the hover applies to, when the server supplied one. */
|
||||
readonly range?: LspRange
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* The closed result union. Navigation operations (`goToDefinition`, `findReferences`,
|
||||
* `goToImplementation`) normalize to `locations`; `hover` normalizes to content or `null`.
|
||||
* Consumers `switch` on `kind` to exhaustiveness so a new arm breaks compilation until handled.
|
||||
*
|
||||
* The `locations` variant carries `resolvedWorkspaceRoot`: the provider's canonical form of the
|
||||
* request's `workspaceRoot`, and the root its `file:` location URIs are relative to. A caller that
|
||||
* relativizes display paths MUST use this, not the request's (possibly symlinked) `workspaceRoot`;
|
||||
* otherwise a symlinked workspace misclassifies in-workspace results as external.
|
||||
*/
|
||||
type LspQueryResult =
|
||||
| { readonly kind: 'locations'; readonly locations: readonly LspLocation[]; readonly resolvedWorkspaceRoot: string }
|
||||
| { readonly kind: 'hover'; readonly hover: LspHover | null }
|
||||
```
|
||||
|
||||
## 提供方与服务
|
||||
|
||||
每个提供方拥有一个稳定的品牌化 `id`,以及一份互斥的、小写且以点开头的扩展名映射。`registerProvider` 会原子保留 id 和每个扩展名:注册无效或冲突时不发布任何内容;其 disposer 会释放所有保留项。每次查询独立选择提供方,且选择与顺序无关;没有匹配项时抛出 `LspError` `LSP_UNAVAILABLE`。该 seam 不公开协议类型、进程或文档控制,也不提供通用 JSON-RPC 逃生口。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* A language-server backend registered on `ctx.lsp`. Each provider owns a stable {@link
|
||||
* LspProviderId} and an extension-to-language-id map (lowercase, leading-dot keys).
|
||||
* `findReferences` always includes declarations — the provider enforces this internally; callers
|
||||
* get no flag.
|
||||
*/
|
||||
interface LspProvider {
|
||||
/** Stable provider identity, reserved atomically with the extension mappings. */
|
||||
readonly id: LspProviderId
|
||||
/** Lowercase leading-dot extension → LSP language id (e.g. `{ '.ts': 'typescript' }`). */
|
||||
readonly extensionToLanguage: Readonly<Record<string, string>>
|
||||
/**
|
||||
* Run one query. The seam has already selected this provider and derived `languageId`.
|
||||
* @param request - the resolved provider query (caller request + derived language id).
|
||||
* @param signal - optional cancellation; the provider stops its own work when it aborts.
|
||||
* @returns the normalized, closed-union result.
|
||||
*/
|
||||
query(request: LspProviderQuery, signal?: AbortSignal): Promise<LspQueryResult>
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* The LSP capability seam (`ctx.lsp`). Owns provider registration/selection and normalized query
|
||||
* execution; exposes exactly the four operations and no protocol escape hatch.
|
||||
*/
|
||||
interface LspService {
|
||||
/**
|
||||
* Register a provider, atomically reserving its id and every normalized extension. Any conflict
|
||||
* or invalid input publishes nothing and throws `LspError`; the returned disposer releases all
|
||||
* reservations. Disposed with the calling fiber.
|
||||
* @param provider - the backend to register.
|
||||
* @returns a synchronous disposer releasing the id and all extension reservations.
|
||||
*/
|
||||
registerProvider(provider: LspProvider): () => void
|
||||
/**
|
||||
* Select a provider by the file's extension and run one query. Selection is per-query and
|
||||
* order-independent; no match throws `LspError` `LSP_UNAVAILABLE`.
|
||||
* @param request - the normalized query.
|
||||
* @param signal - optional cancellation forwarded to the selected provider.
|
||||
* @returns the normalized, closed-union result.
|
||||
*/
|
||||
query(request: LspQueryRequest, signal?: AbortSignal): Promise<LspQueryResult>
|
||||
}
|
||||
```
|
||||
|
||||
`LspProviderId` 是该 seam 的品牌化 id(来自 [dsh-brand](../../packages/util/brand) 的 `Branded<'LspProviderId'>`);`LspError` 扩展 `HarnessError`,提供 `LSP_INVALID_PROVIDER`、`LSP_CONFLICT`、`LSP_UNAVAILABLE`、`LSP_DISPOSED`、`LSP_UNSUPPORTED_OPERATION` 和 `LSP_MALFORMED_RESPONSE` 等稳定错误码,调用方应按错误码路由,而不是解析 `message`。
|
||||
6
docs/core-data-structures/pty.i18n.yaml
Normal file
6
docs/core-data-structures/pty.i18n.yaml
Normal file
@@ -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
|
||||
pty.md: 97e1e662d1128ab0555e34f8284cf69d7d9d0d1a
|
||||
pty.zh.md: b17bc0d2c7bdb2a980df36824bd360ea975967f5
|
||||
@@ -1,5 +1,7 @@
|
||||
# Persistent PTY Sessions
|
||||
|
||||
English | [中文](pty.zh.md)
|
||||
|
||||
Types shared by PTY backends, `ctx.pty`, and the model-facing consumer. The [persistent PTY Agent Note](../../.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.md) owns the rationale; this page records the cross-package vocabulary from [`packages/pty/pty/src/types.ts`](../../packages/pty/pty/src/types.ts).
|
||||
|
||||
## Identity and readiness
|
||||
|
||||
91
docs/core-data-structures/pty.zh.md
Normal file
91
docs/core-data-structures/pty.zh.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# 持久 PTY 会话
|
||||
|
||||
[English](pty.md) | 中文
|
||||
|
||||
PTY 后端、`ctx.pty` 与面向模型的消费方共享的类型。[持久 PTY Agent Note(agent 决策记录)](../../.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.md) 负责记录决策依据;本页记录来自 [`packages/pty/pty/src/types.ts`](../../packages/pty/pty/src/types.ts) 的跨包(package)词汇。
|
||||
|
||||
## 标识与就绪
|
||||
|
||||
`PtySessionId` 是由服务铸造的品牌化 id。可选名称是拥有者本地的显示元数据;授权比较的是确切的所属 `Agent`,而不是名称或猜测的 id。
|
||||
|
||||
`PtyWaitReason` 说明一次发送为何返回。它与 `PtySessionStatus` 无关:一次发送可能因静默或超时而返回,但顶层 shell 仍然存活;`session_exit` 表示该 shell 已退出,而不是某个任意的前台子进程已退出。
|
||||
|
||||
```ts type-equiv
|
||||
/** Why one interactive send returned control to its caller. */
|
||||
type PtyWaitReason = 'stdin_read' | 'inferred_idle' | 'timeout' | 'session_exit'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Top-level PTY process status, independent of a send's wait reason. */
|
||||
type PtySessionStatus =
|
||||
| { kind: 'running' }
|
||||
| { kind: 'exited'; exitCode: number | null; signal: NodeJS.Signals | null }
|
||||
```
|
||||
|
||||
## 后端与活跃会话
|
||||
|
||||
后端负责某个已注册类型的启动方式和就绪检测。`PtyService` 只在初始化成功后才发布返回的会话,随后负责 id 授权与清理。无法清理部分启动资源的后端会以 `PtyBackendCleanupError` 拒绝,从而让资源释放流程保留该清理失败,同时不替换调用方的取消原因。后端会话拥有终端状态,并负责使已捕获资源完全停稳。
|
||||
|
||||
```ts type-equiv
|
||||
/** Replaceable provider for one PTY session type. */
|
||||
interface PtyBackend {
|
||||
/** Stable type selected by {@link PtySpawnRequest.type}. */
|
||||
readonly type: string
|
||||
/** Create an unpublished session or reject after cleaning partial resources; cleanup failure uses {@link PtyBackendCleanupError}. */
|
||||
spawn(spec: PtyBackendSpawnSpec): Promise<PtyBackendSession>
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Backend-owned live session retained by {@link PtyService}. */
|
||||
interface PtyBackendSession {
|
||||
/** Initial bounded terminal output returned from `terminal_open`. */
|
||||
readonly motd: string
|
||||
/** Top-level process id when one exists. */
|
||||
readonly pid?: number
|
||||
/** Start one exclusive send operation. */
|
||||
startSend(request: PtySendRequest): PtySendOperation
|
||||
/** Read one bounded page from retained scrollback. */
|
||||
read(request: PtyReadRequest): PtyReadResult
|
||||
/** Signal the verified foreground process group. */
|
||||
signal(signal: PtySignal): Promise<PtySignalResult>
|
||||
/** Observe top-level process status. */
|
||||
status(): PtySessionStatus
|
||||
/** Idempotently close the captured owned process tree and await quiescence. */
|
||||
close(reason: string): Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
## 发送与保留输出
|
||||
|
||||
一个活跃会话同时只接受一个活动发送。该操作向通用后台任务公开一个消费式输出游标,并向前台调用方公开一个最终结果。`PtyReadResult` 则为有界的会话 scrollback 单独分页。
|
||||
|
||||
```ts type-equiv
|
||||
/** Live backend-owned send; exactly one may be active per PTY session. */
|
||||
interface PtySendOperation {
|
||||
/** Resolves after readiness, timeout, cancellation, or top-level process exit. */
|
||||
done: Promise<PtySendResult>
|
||||
/** Consume output produced since the prior call. */
|
||||
readOutput(): PtySendRead
|
||||
/** Request `SIGINT`; returns false after the operation settled. */
|
||||
cancel(): boolean
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Settled result for one foreground or background send. */
|
||||
interface PtySendResult {
|
||||
/** Bounded rendered terminal delta remaining at settlement. */
|
||||
viewport: string
|
||||
/** Why the wait returned; this does not imply arbitrary child-process exit. */
|
||||
waitReason: PtyWaitReason
|
||||
/** Top-level session status observed at settlement. */
|
||||
sessionStatus: PtySessionStatus
|
||||
/** Whether output was dropped from the operation or retained scrollback. */
|
||||
truncated: boolean
|
||||
}
|
||||
```
|
||||
|
||||
## 归属与持久性
|
||||
|
||||
`PtyService` 会将一项等待完成的清理附加到确切的拥有者作用域,拒绝其他拥有者的操作,并让会话在后端或工具插件重载期间保持存活。PTY 状态与原始字节仍局限在进程内。模型输入与有界的返回输出通过现有 `tool/call`、`tool/result` 和任务结果路径持久保存,而不是重复记录 PTY 会话事件。
|
||||
6
docs/core-data-structures/session-reference.i18n.yaml
Normal file
6
docs/core-data-structures/session-reference.i18n.yaml
Normal file
@@ -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
|
||||
session-reference.md: 4898cdd641427a023dde63bfc9759300964c7fac
|
||||
session-reference.zh.md: 8e36d70241f2565a587bd3c1ee270d99dba47d71
|
||||
@@ -1,5 +1,7 @@
|
||||
# Session References
|
||||
|
||||
English | [中文](session-reference.zh.md)
|
||||
|
||||
Structured cross-session reference requests and prepared message contexts. The [package contract](../../packages/context/session-reference) owns canonical URIs, current-surface projection, tag-safe JSON and byte retention, stable errors, and the untrusted model prompt. Host adapters use these types instead of passing their UI mention syntax into the agent core.
|
||||
|
||||
Source: [`packages/context/session-reference/src/types.ts`](../../packages/context/session-reference/src/types.ts)
|
||||
|
||||
67
docs/core-data-structures/session-reference.zh.md
Normal file
67
docs/core-data-structures/session-reference.zh.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# 会话引用
|
||||
|
||||
[English](session-reference.md) | 中文
|
||||
|
||||
结构化的跨会话引用请求与预备消息上下文。[包(package)契约](../../packages/context/session-reference) 负责规范 URI、当前表层投影、标签安全的 JSON 与字节保留、稳定错误和不可信的模型提示词。宿主适配器使用这些类型,而不会把各自 UI 的提及语法传入 agent(智能体)核心。
|
||||
|
||||
来源:[`packages/context/session-reference/src/types.ts`](../../packages/context/session-reference/src/types.ts)
|
||||
|
||||
## 输入与候选项
|
||||
|
||||
`SessionReferenceInput` 是与宿主无关的选择。id 具有权威性;label 是随快照携带的显示元数据。
|
||||
|
||||
```ts type-equiv
|
||||
/** One source session selected by a host. */
|
||||
interface SessionReferenceInput {
|
||||
/** Opaque source session identity. */
|
||||
sessionId: SessionId
|
||||
/** Optional user-facing mention label. */
|
||||
label?: string
|
||||
}
|
||||
```
|
||||
|
||||
`SessionReferenceCandidate` 是面向宿主的发现输出。存在最新会话标题时,它的 label 使用该标题;筛选仍只搜索 session id 和 cwd,绝不搜索 transcript(文本记录)。
|
||||
|
||||
```ts type-equiv
|
||||
/** One host-facing candidate from exact session metadata. */
|
||||
interface SessionReferenceCandidate {
|
||||
/** Opaque source session identity. */
|
||||
sessionId: SessionId
|
||||
/** Latest log-backed title, falling back to the opaque session id. */
|
||||
label: string
|
||||
/** Source session working directory, when recorded. */
|
||||
cwd?: string
|
||||
/** Source session creation time in Unix epoch milliseconds. */
|
||||
createdAt: number
|
||||
}
|
||||
```
|
||||
|
||||
## 预备消息
|
||||
|
||||
预备过程保留可读的当前消息内容,并最多返回一个聚合上下文。宿主会把 `contexts` 绑定到该次确切的 `followup()` 或 `steer()` 调用。
|
||||
|
||||
```ts type-equiv
|
||||
/** Message payload and the zero-or-one durable snapshot contexts bound to it. */
|
||||
interface PreparedReferencedMessage {
|
||||
/** Readable message content after host mention tokens are removed. */
|
||||
content: ContentBlock[]
|
||||
/** Empty without references; otherwise one aggregated untrusted context. */
|
||||
contexts: HookContext[]
|
||||
}
|
||||
```
|
||||
|
||||
## 错误
|
||||
|
||||
`SessionReferenceError.code` 区分无效配置或输入、自引用、数量限制、源读取失败、预算失败和取消。宿主协议会把这些 code 映射到各自的错误信封,无需检查提示词字节。
|
||||
|
||||
```ts type-equiv
|
||||
/** Stable failure codes exposed to host adapters. */
|
||||
type SessionReferenceErrorCode =
|
||||
| 'SESSION_REFERENCE_INVALID_CONFIG'
|
||||
| 'SESSION_REFERENCE_INVALID_REFERENCE'
|
||||
| 'SESSION_REFERENCE_SELF_REFERENCE'
|
||||
| 'SESSION_REFERENCE_TOO_MANY'
|
||||
| 'SESSION_REFERENCE_READ_FAILED'
|
||||
| 'SESSION_REFERENCE_BUDGET_EXCEEDED'
|
||||
| 'SESSION_REFERENCE_CANCELLED'
|
||||
```
|
||||
6
docs/core-data-structures/session-title.i18n.yaml
Normal file
6
docs/core-data-structures/session-title.i18n.yaml
Normal file
@@ -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
|
||||
session-title.md: 6575bda5fdecf2be15ed7c3288efb0efa759ac8a
|
||||
session-title.zh.md: 66ec567ac6cef32acef9c50ae3e67155097a76b6
|
||||
@@ -1,5 +1,7 @@
|
||||
# Session Titles
|
||||
|
||||
English | [中文](session-title.zh.md)
|
||||
|
||||
Durable latest-wins title state and the optional asynchronous provider vocabulary owned by [`@deepseek-ai/dsh-session-title`](../../packages/session-title/session-title). The shared LLM helper owns the exact auxiliary request record. Package READMEs own timing, fallback, failure, and fork behavior; the generated [persistence catalog](../persistence-catalog.md) owns the complete event declarations.
|
||||
|
||||
Sources: [`packages/session-title/session-title/src/index.ts`](../../packages/session-title/session-title/src/index.ts), [`packages/session-title/session-title-llm/src/index.ts`](../../packages/session-title/session-title-llm/src/index.ts)
|
||||
|
||||
142
docs/core-data-structures/session-title.zh.md
Normal file
142
docs/core-data-structures/session-title.zh.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# 会话标题
|
||||
|
||||
[English](session-title.md) | 中文
|
||||
|
||||
[`@deepseek-ai/dsh-session-title`](../../packages/session-title/session-title) 所拥有的持久化后写覆盖标题状态与可选异步提供方词汇。共享 LLM(大语言模型)辅助组件负责精确的辅助请求记录。各包(package)README 负责时序、回退、失败与 fork 行为;生成的[持久化日志事件目录](../persistence-catalog.md)负责完整的事件声明。
|
||||
|
||||
源码:[`packages/session-title/session-title/src/index.ts`](../../packages/session-title/session-title/src/index.ts)、[`packages/session-title/session-title-llm/src/index.ts`](../../packages/session-title/session-title-llm/src/index.ts)
|
||||
|
||||
## 持久标题状态
|
||||
|
||||
提供方生成修订时会记录 `SessionTitleProviderId`。`SessionTitleEventData` 携带精确的人类消息来源信息,`SessionTitleSnapshot` 则加入 `foldSessionTitle()` 选出的持久事件信封事实。
|
||||
|
||||
```ts type-equiv
|
||||
/** Identifies one session-title provider registration. */
|
||||
type SessionTitleProviderId = Branded<'SessionTitleProviderId'>
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Exact auxiliary model route that produced a title. */
|
||||
interface SessionTitleModelProvenance {
|
||||
/** Registered LLM provider route. */
|
||||
readonly provider: string
|
||||
/** Provider model id. */
|
||||
readonly model: string
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Durable ownership record for an accepted session title. */
|
||||
type SessionTitleSource =
|
||||
| { readonly kind: 'fallback' }
|
||||
| {
|
||||
readonly kind: 'provider'
|
||||
readonly provider: SessionTitleProviderId
|
||||
readonly model?: SessionTitleModelProvenance
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Payload of the log-only `session/title` event. */
|
||||
interface SessionTitleEventData {
|
||||
/** Normalized non-empty title text. */
|
||||
readonly title: string
|
||||
/** Exact human `user/message` seqs used to derive this title. */
|
||||
readonly messageSeqs: number[]
|
||||
/** Built-in fallback or registered-provider provenance. */
|
||||
readonly source: SessionTitleSource
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Latest folded title plus the title event's durable envelope facts. */
|
||||
interface SessionTitleSnapshot extends SessionTitleEventData {
|
||||
/** Seq of the latest `session/title` event. */
|
||||
readonly eventSeq: number
|
||||
/** Timestamp of the latest `session/title` event. */
|
||||
readonly updatedAt: number
|
||||
}
|
||||
```
|
||||
|
||||
## 辅助请求记录
|
||||
|
||||
共享 LLM 辅助组件会在调用模型前,记录每一项已经过验证且可分发的标题请求。即使后续生成失败,载荷仍会复现模型可见的系统输入与消息输入、路由、输出上限、提供方归属和源消息归因。
|
||||
|
||||
```ts type-equiv
|
||||
/** Exact model-visible request recorded before one auxiliary title dispatch. */
|
||||
interface SessionTitleLlmRequestEventData {
|
||||
/** Registered title-provider identity responsible for the request. */
|
||||
readonly titleProvider: SessionTitleProviderId
|
||||
/** Exact human `user/message` seqs represented in `messages`. */
|
||||
readonly messageSeqs: number[]
|
||||
/** Exact auxiliary LLM route. */
|
||||
readonly route: SessionTitleModelProvenance
|
||||
/** Exact auxiliary system prompt. */
|
||||
readonly system: string
|
||||
/** Exact auxiliary message list. */
|
||||
readonly messages: Message[]
|
||||
/** Exact auxiliary output-token cap. */
|
||||
readonly maxTokens: number
|
||||
}
|
||||
```
|
||||
|
||||
## 提供方输入与输出
|
||||
|
||||
服务会对截至某一修订的合格消息创建快照。提供方返回的 seq 仅可来自该请求;由服务负责的接受过程会验证顺序、规范化标题、强制执行字节上限并追加来源信息。
|
||||
|
||||
```ts type-equiv
|
||||
/** One eligible human text message exposed to title providers. */
|
||||
interface SessionTitleUserMessage {
|
||||
/** Source `user/message` event seq. */
|
||||
readonly seq: number
|
||||
/** Exact concatenated text-block content. */
|
||||
readonly text: string
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Automatic generation cadence owned by a registered provider. */
|
||||
type SessionTitleAutomaticMode = 'first-message' | 'all-user-messages'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Immutable input supplied to one title-provider call. */
|
||||
interface SessionTitleProviderRequest {
|
||||
/** Live session being titled. */
|
||||
readonly session: Session
|
||||
/** All eligible human messages through this generation revision. */
|
||||
readonly messages: readonly SessionTitleUserMessage[]
|
||||
/** Exact current logged main-request route, when one has been recorded. */
|
||||
readonly route?: SessionTitleModelProvenance
|
||||
/** Cancellation for supersession, disposal, timeout composition, or the explicit caller. */
|
||||
readonly signal: AbortSignal
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Provider output before service-owned normalization and durable acceptance. */
|
||||
interface SessionTitleProviderResult {
|
||||
/** Proposed title text. */
|
||||
readonly title: string
|
||||
/** Exact seqs from `request.messages` used by this result. */
|
||||
readonly messageSeqs: readonly number[]
|
||||
/** Auxiliary LLM route, when generation used a model. */
|
||||
readonly model?: SessionTitleModelProvenance
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** One optional asynchronous title implementation registered with the service. */
|
||||
interface SessionTitleProvider {
|
||||
/** Stable provider identity recorded in title provenance. */
|
||||
readonly id: SessionTitleProviderId
|
||||
/** When new human prompts start automatic generation. */
|
||||
readonly automatic: SessionTitleAutomaticMode
|
||||
/**
|
||||
* Produce one title revision.
|
||||
* @param request - message snapshot, current route, session, and cancellation.
|
||||
* @returns proposed title plus exact input seqs and optional model provenance.
|
||||
*/
|
||||
generate(request: SessionTitleProviderRequest): Promise<SessionTitleProviderResult>
|
||||
}
|
||||
```
|
||||
6
docs/core-data-structures/spill.i18n.yaml
Normal file
6
docs/core-data-structures/spill.i18n.yaml
Normal file
@@ -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
|
||||
spill.md: a798d8143b2849dc0cf49d04e7019ce796cdee45
|
||||
spill.zh.md: 1af6939d1d8fd37958cae4f9cf2cbf706b17acd0
|
||||
@@ -1,5 +1,7 @@
|
||||
# Spill Storage
|
||||
|
||||
English | [中文](spill.zh.md)
|
||||
|
||||
The spill storage seam — a [capability seam](../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md) that persists a tool's oversized text and returns a model-facing locator plus retrieval guidance, split across packages: interface ([dsh-spill](../../packages/spill/spill), `ctx.spillStore`), implementation ([dsh-spill-local](../../packages/spill/spill-local), private session-scoped files on the host filesystem), and consumer ([dsh-spill-policy](../../packages/spill/spill-policy), the `tools/post-execute` policy). Spill is **one optional capability**, not part of the agent-loop spine — so its vocabulary lives here, not in [core.md](core.md). Preview mechanics stay in [dsh-retention](../../packages/util/retention); this seam only saves the final text the policy hands it.
|
||||
|
||||
Source: [`packages/spill/spill/src/types.ts`](../../packages/spill/spill/src/types.ts)
|
||||
|
||||
85
docs/core-data-structures/spill.zh.md
Normal file
85
docs/core-data-structures/spill.zh.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# 落盘存储
|
||||
|
||||
[English](spill.md) | 中文
|
||||
|
||||
落盘存储 seam 是一项[能力 seam](../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md),它持久保存工具的超大文本,并返回面向模型的定位符与检索指引;该能力拆分到三个包(package):接口([dsh-spill](../../packages/spill/spill),`ctx.spillStore`)、实现([dsh-spill-local](../../packages/spill/spill-local),宿主文件系统中会话作用域的私有文件)和消费方([dsh-spill-policy](../../packages/spill/spill-policy),`tools/post-execute` 策略)。落盘是**一项可选能力**,不属于 agent loop(智能体循环)主干,因此其词汇记录在此处,而不在 [core.md](core.md) 中。预览机制仍归 [dsh-retention](../../packages/util/retention) 所有;该 seam 只保存策略交给它的最终文本。
|
||||
|
||||
源码:[`packages/spill/spill/src/types.ts`](../../packages/spill/spill/src/types.ts)
|
||||
|
||||
## 保存请求
|
||||
|
||||
`saveText` 是整个 seam:原样持久保存 `content`,并返回不透明的定位符、后端提供的检索提示和准确字节数。请求携带保存时的存储命名空间(`owner`)、内容来源(`source`,用于命名和检查的描述性来源信息,而非访问控制)以及后端可用作命名提示的 `suggestedName`(它不是路径)。
|
||||
|
||||
```ts type-equiv
|
||||
/** One request to persist text to a spill artifact. */
|
||||
interface SaveTextSpill {
|
||||
owner: SpillOwner
|
||||
source: SpillSource
|
||||
/**
|
||||
* A caller-suggested base name (e.g. `web_fetch.txt`). The backend sanitizes
|
||||
* it to a single safe path segment before use — it is a hint, never a path.
|
||||
*/
|
||||
suggestedName: string
|
||||
/** The full text to persist (UTF-8). */
|
||||
content: string
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Save-time storage namespace for a spilled artifact. The session id lets a
|
||||
* backend group storage under the producing session, but the returned
|
||||
* {@link SpillLocator} is the model-facing handle. Forked sessions inherit
|
||||
* locators already present in the seeded log; those artifacts are not copied or
|
||||
* re-owned, and spills produced after the fork use the child session id.
|
||||
*/
|
||||
interface SpillOwner {
|
||||
sessionId: SessionId
|
||||
}
|
||||
```
|
||||
|
||||
`SpillOwner.sessionId` 是保存时的存储命名空间。fork 后的会话会从种子日志继承已有的落盘定位符;这些产物不会被复制或重新取得所有权,fork 后产生的落盘则使用子会话 id。保留期清理可以连同其他旧会话产物一起使旧定位符失效;落盘 seam 不定义逐会话的清理策略。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Provenance of one spilled artifact — recorded by the backend for a readable
|
||||
* filename and inspection. Not interpreted for access control; purely
|
||||
* descriptive.
|
||||
*/
|
||||
interface SpillSource {
|
||||
/** The tool whose result was spilled (e.g. `web_fetch`). */
|
||||
toolName: string
|
||||
/** The model-issued call id the result belongs to. */
|
||||
callId: CallId
|
||||
/** A short human label for the artifact (e.g. `result`). */
|
||||
label: string
|
||||
}
|
||||
```
|
||||
|
||||
## 结果
|
||||
|
||||
```ts type-equiv
|
||||
/** A saved spill artifact: its locator, byte length, and backend-specific retrieval guidance. */
|
||||
interface SpillRef {
|
||||
locator: SpillLocator
|
||||
bytes: number
|
||||
retrievalHint: string
|
||||
}
|
||||
```
|
||||
|
||||
`SpillLocator` 是后端返回的[品牌化](core.md#branded-ids)面向模型句柄。本地后端将它渲染为文件系统路径;远程或数据库后端可以渲染 URI、键或命令 token。消费方将它视为不透明值,并使用 `retrievalHint` 渲染,而不是假定 `read` 始终是正确的检索机制。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Opaque model-facing handle for one spilled artifact. A local backend may use a
|
||||
* filesystem path; a remote or database backend may use a URI or key. Consumers
|
||||
* render it with {@link SpillRef.retrievalHint}, but do not parse it.
|
||||
*/
|
||||
type SpillLocator = Branded<'SpillLocator'>
|
||||
```
|
||||
|
||||
## 服务
|
||||
|
||||
`SpillStore`(`ctx.spillStore`,定义于 [`packages/spill/spill/src/index.ts`](../../packages/spill/spill/src/index.ts))是只有一个方法的抽象服务:`saveText(input) → Promise<SpillRef>`。它持久保存完整的 `content`,并在实际存储失败(权限、ENOSPC、后端不可用)时拒绝。该 seam 只负责存储:不负责保留策略、工具结果替换或检索/搜索 API。
|
||||
|
||||
本地后端([dsh-spill-local](../../packages/spill/spill-local))写入 `<root>/session-<hash>/<random>-<safeName>`:根目录是已配置或延迟创建的私有(0700)目录,会话子目录采用 `sha256(sessionId)`,并通过排他的仅所有者可访问写入(`open(path, 'wx', 0o600)`)防止预先植入的符号链接重定向写入。其 `locator` 是本地路径,`retrievalHint` 则告知模型在该路径上使用 `read` 或 `grep`。策略消费方([dsh-spill-policy](../../packages/spill/spill-policy))会把超过 `maxInlineBytes` 的纯文本最终结果替换为保留库生成的首尾预览和落盘引用;该过程尽力而为:保存失败时保留原始内联结果,而不会把成功的调用变成 `isError`。
|
||||
6
docs/core-data-structures/tasks.i18n.yaml
Normal file
6
docs/core-data-structures/tasks.i18n.yaml
Normal file
@@ -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
|
||||
tasks.md: d1f5a6d7b369e6113132f60e493cf87757e20599
|
||||
tasks.zh.md: 1562d9401f0f55ac6d6260902b8b1c71d9664d48
|
||||
@@ -1,5 +1,7 @@
|
||||
# Background Task Runtime
|
||||
|
||||
English | [中文](tasks.zh.md)
|
||||
|
||||
Types shared by long-running producers, `ctx.tasks`, and task control surfaces. The [runtime Agent Note](../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md) owns the design; this page records the literal shapes from [`packages/tasks/tasks/src/types.ts`](../../packages/tasks/tasks/src/types.ts).
|
||||
|
||||
## Ids and status
|
||||
|
||||
154
docs/core-data-structures/tasks.zh.md
Normal file
154
docs/core-data-structures/tasks.zh.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# 后台任务运行时
|
||||
|
||||
[English](tasks.md) | 中文
|
||||
|
||||
长时间运行的生产方、`ctx.tasks` 与任务控制接口共用的类型。[运行时 Agent Note(agent 决策记录)](../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md)负责设计;本页记录 [`packages/tasks/tasks/src/types.ts`](../../packages/tasks/tasks/src/types.ts) 中的字面形状。
|
||||
|
||||
## ID 与状态
|
||||
|
||||
`TaskId` 是按 `<kind>-N` 生成的[品牌化 id](core.md#branded-ids)。访问控制依赖拥有者授权,而非 id 的保密性。`TaskKind` 派生自可合并扩展的 map;注册表将各个 kind 视为不透明的 id 命名空间。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Producer-defined task kinds. Plugins extend this map by declaration merging;
|
||||
* the registry treats every value as an opaque id namespace.
|
||||
*/
|
||||
interface TaskKindMap {
|
||||
bash: 'bash'
|
||||
subagent: 'subagent'
|
||||
}
|
||||
```
|
||||
|
||||
`TaskStatus` 为 `'running' | 'stopping' | 'completed' | 'killed' | 'failed'`;生产方特有的事实归入 `TaskSnapshot.detail`。
|
||||
|
||||
## 生产方契约
|
||||
|
||||
`TaskStart` 声明身份和启动器。运行时会在调用 `run()` 前完成预检,随后提交注册,不再执行可能失败的步骤。生产方拥有执行资源;运行时拥有身份、访问权限和生命周期状态。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Producer declaration passed to {@link TaskService.start}. The runtime
|
||||
* preflights access and cleanup before invoking {@link run}; the producer owns
|
||||
* execution resources while the runtime owns identity and lifecycle state.
|
||||
*/
|
||||
interface TaskStart {
|
||||
/** Producer kind — also the id prefix (`bash`, `subagent`, …). */
|
||||
kind: TaskKind
|
||||
/** One-line model-facing label (the command; the delegation description). */
|
||||
label: string
|
||||
/**
|
||||
* Optional UTF-8 byte cap for each complete model-facing completion notice or
|
||||
* output read, including control-surface status metadata.
|
||||
*/
|
||||
outputLimitBytes?: number
|
||||
/**
|
||||
* Owning live agent. Access is fenced by its session id, and agent disposal
|
||||
* cancels and awaits the task. The instance must be the one currently
|
||||
* registered under its agent id. Omitting the owner creates an unowned task,
|
||||
* open to any caller until service disposal.
|
||||
*/
|
||||
owner?: Agent
|
||||
/**
|
||||
* Start the work after preflight and synchronously return its hooks. Called
|
||||
* once; a throw leaves nothing registered, and the producer must clean up any
|
||||
* partially started resources.
|
||||
*/
|
||||
run(): TaskHooks
|
||||
}
|
||||
```
|
||||
|
||||
`TaskHooks.done` 是完全停稳边界。可选的 `readOutput` 用来区分会消费输出的流式任务和仅有最终输出的任务。
|
||||
|
||||
```ts type-equiv
|
||||
/** Hooks through which the runtime controls and observes producer work. */
|
||||
interface TaskHooks {
|
||||
/**
|
||||
* Request termination. Must be synchronous, idempotent, and eventually settle
|
||||
* {@link done}; throws propagate. The optional reason is forwarded verbatim.
|
||||
*/
|
||||
cancel(reason?: string): void
|
||||
/**
|
||||
* Resolves after the producer releases its resources, not merely when work
|
||||
* finishes. Must not reject; the runtime converts a rejection to `failed`.
|
||||
* If teardown cancellation throws, the runtime may force-fail only the
|
||||
* registry record without claiming that the work stopped.
|
||||
*/
|
||||
done: Promise<TaskOutcome>
|
||||
/**
|
||||
* Consume output produced since the previous call. The producer formats
|
||||
* truncation and spill notices. Absence marks a final-output-only task; each
|
||||
* task has one consuming cursor.
|
||||
*/
|
||||
readOutput?(): string
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Terminal result supplied by a producer through {@link TaskHooks.done}. */
|
||||
interface TaskOutcome {
|
||||
/** How the task ended: finished (`completed`), cancelled (`killed`), or broke (`failed`). */
|
||||
status: 'completed' | 'killed' | 'failed'
|
||||
/** Kind-specific detail rendered into status lines ('exit code: 3', 'max-tokens'). */
|
||||
detail?: string
|
||||
/** Final output for tasks without `readOutput`; stream tasks leave it unset. */
|
||||
output?: string
|
||||
}
|
||||
```
|
||||
|
||||
## 消费方视图
|
||||
|
||||
快照是每次新建的只读投影。`ownerSession` 携带用于授权的共享 `SessionId`;完成监听器则会另行收到用于生命周期清理的确切拥有者对象。另一个接口已经交付终止状态或承诺交付时,`reported` 会抑制完成通知。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* A read-only projection of one task, safe to hand to listeners and tools —
|
||||
* a fresh object per call, never live registry state.
|
||||
*/
|
||||
interface TaskSnapshot {
|
||||
/** The registry-issued id (`<kind>-N`). */
|
||||
id: TaskId
|
||||
/** The producer kind the task was registered with. */
|
||||
kind: TaskKind
|
||||
/** The producer-supplied one-line label. */
|
||||
label: string
|
||||
/** Producer-owned cap for complete model-facing notices and output reads. */
|
||||
outputLimitBytes?: number
|
||||
/**
|
||||
* Owner session id used for authorization and correlation; absent for
|
||||
* unowned tasks. Completion listeners receive the exact {@link Agent}
|
||||
* separately through {@link TaskDoneListener}.
|
||||
*/
|
||||
ownerSession?: SessionId
|
||||
/** Current lifecycle state. */
|
||||
status: TaskStatus
|
||||
/** Kind-specific status detail, present once the producer supplied one (usually terminal). */
|
||||
detail?: string
|
||||
/** Epoch ms when the task was registered. */
|
||||
startedAt: number
|
||||
/** Epoch ms when the task settled; absent while `running`/`stopping`. */
|
||||
finishedAt?: number
|
||||
/**
|
||||
* True when a kill, read, or wait has reported or committed to report the
|
||||
* terminal state. Completion surfaces suppress redundant notices when set.
|
||||
*/
|
||||
reported: boolean
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Output and post-read state returned by {@link TaskService.read}. */
|
||||
interface TaskRead {
|
||||
/**
|
||||
* Stream kinds: the consuming delta since the previous read. Final-output
|
||||
* kinds: empty while live, the terminal {@link TaskOutcome.output} (or
|
||||
* empty) once settled — idempotent, never consumed.
|
||||
*/
|
||||
text: string
|
||||
/** The task's state at read time. */
|
||||
snapshot: TaskSnapshot
|
||||
}
|
||||
```
|
||||
|
||||
## 服务行为
|
||||
|
||||
[`TaskService`](../../packages/tasks/tasks/src/index.ts) 提供原子 `start`、限定调用方作用域的 `get` 和 `list`、`read`、`kill`、有界 `wait`、故障隔离的 `onTaskDone` 监听器,以及 `attachSurface` 可用性防线。授权会比较拥有者会话;拥有者清理会选择确切的已注册 `Agent` 实例。包(package)契约见 [`dsh-tasks`](../../packages/tasks/tasks/README.md),面向模型的接口见 [`dsh-tool-tasks`](../../packages/tasks/tool-tasks/README.md)。
|
||||
6
docs/core-data-structures/token-meter.i18n.yaml
Normal file
6
docs/core-data-structures/token-meter.i18n.yaml
Normal file
@@ -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
|
||||
token-meter.md: 05784e294485a11acf0e4c8972e4083b1786c943
|
||||
token-meter.zh.md: c0dc55274acf21186f7baa00c377f9135792f888
|
||||
@@ -1,5 +1,7 @@
|
||||
# Token Meter
|
||||
|
||||
English | [中文](token-meter.zh.md)
|
||||
|
||||
`@deepseek-ai/dsh-token-meter` exposes one detached replay snapshot for request pressure and positional surface pricing. `logRevision` is the number of durable events consumed for every field in the measurement.
|
||||
|
||||
Source: [`packages/llm/token-meter/src/types.ts`](../../packages/llm/token-meter/src/types.ts)
|
||||
|
||||
43
docs/core-data-structures/token-meter.zh.md
Normal file
43
docs/core-data-structures/token-meter.zh.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Token 计量
|
||||
|
||||
[English](token-meter.md) | 中文
|
||||
|
||||
`@deepseek-ai/dsh-token-meter` 公开一个独立的回放快照,用于表示请求压力与按位置计算的 surface 定价。`logRevision` 表示生成该计量中每个字段时所消费的持久事件数量。
|
||||
|
||||
来源:[`packages/llm/token-meter/src/types.ts`](../../packages/llm/token-meter/src/types.ts)
|
||||
|
||||
## `TokenMeasurement`
|
||||
|
||||
```ts type-equiv
|
||||
/** Detached immutable request-pressure and surface snapshot at one consumed log revision. */
|
||||
interface TokenMeasurement {
|
||||
/** Number of durable events consumed; equal to the next unread event seq. */
|
||||
readonly logRevision: number
|
||||
/** Provider or heuristic anchor used for this measurement. */
|
||||
readonly baseline: TokenMeasurementBaseline
|
||||
/** Signed repricing of current surface content relative to the baseline anchor. */
|
||||
readonly surfaceDeltaTokens: number
|
||||
/** Non-negative current request-and-response pressure. */
|
||||
readonly totalTokens: number
|
||||
/** Total heuristic tokens across the current surface. */
|
||||
readonly surfaceTokens: number
|
||||
/** Current surface nodes in positional head-to-tail order. */
|
||||
readonly nodes: readonly TokenSurfaceNode[]
|
||||
}
|
||||
```
|
||||
|
||||
`baseline.kind === 'usage'` 表示最近一次成功的提供方调用具有相同的规范请求信封,且当前总量不低于该调用的完整启发式锚点。`estimated` 表示不存在可复用的保守 usage 锚点,因此服务使用固定启发式规则对完整信封和 surface 定价。后续成功请求会替换早先的锚点;有符号的 `surfaceDeltaTokens` 会保留相对于匹配锚点的增长与缩减。`totalTokens` 仍表示请求与响应压力,`surfaceTokens` 则是仅针对 surface 的启发式总量,等于所有节点价格之和。
|
||||
|
||||
## `TokenSurfaceNode`
|
||||
|
||||
```ts type-equiv
|
||||
/** One token-priced node in the current ordered session surface. */
|
||||
interface TokenSurfaceNode {
|
||||
/** Durable sequence number of the surface event. */
|
||||
readonly seq: number
|
||||
/** Heuristic tokens for the exact message projected by this node. */
|
||||
readonly tokens: number
|
||||
}
|
||||
```
|
||||
|
||||
surface 顺序具有权威性;替换节点的持久 seq 可能高于位置排在其后的节点。该快照不可变,不会随底层回放折叠推进而增长。
|
||||
6
docs/web-styling.i18n.yaml
Normal file
6
docs/web-styling.i18n.yaml
Normal file
@@ -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
|
||||
web-styling.md: af05faca30fc968828f5a850f59d9d48ae382b05
|
||||
web-styling.zh.md: d0838cd8a6ee4290cdddff16b979950bec396314
|
||||
@@ -1,107 +1,109 @@
|
||||
# Web GUI 样式规范
|
||||
# Web GUI Style Guide
|
||||
|
||||
> **【token 体系已换代——§1 表格仅历史参考】** 本文的 `--bg-*`/`--text-*`/`--accent` token 族与其宿主包 `packages/client/web-ui` 已随插件化重构退役。现行 token 唯一来源=`packages/client/ui-theme/src/styles/` 的 `--dsw-*` 体系(static 色阶+alias 语义层,暗色=`body[data-ds-dark-theme]` 覆写),sheet 即权威、组件对账以它为准。**仍然有效**:工程约束(CSS Modules + clsx、无组件库、无 tailwind、组件禁 hardcode 色值)、字号成对写行高、间距 4 倍数、代码字体栈末位不放 monospace。
|
||||
English | [中文](web-styling.zh.md)
|
||||
|
||||
> 状态:原「活文档」(随 `packages/client/web-ui` 演进)。视觉基线源自对 deepseekchat 前端仓的实测调研。框架决策与工程约束由 [web-styling-system RFC](../.agents/notes/implemented/process/2026-07-19-web-styling-system.md) 拍板,本文不重复论证。
|
||||
> **[The token system has been replaced—the table in § 1 is retained only for historical reference]** The `--bg-*`/`--text-*`/`--accent` token families documented here and their host package, `packages/client/web-ui`, were retired during the plugin refactor. The sole current token source is `packages/client/ui-theme/src/styles/`, which defines the `--dsw-*` system (a static color scale plus a semantic alias layer, with dark-mode overrides under `body[data-ds-dark-theme]`). The sheet is authoritative and component audits use it as the baseline. **The following rules remain in force**: CSS Modules + clsx, no component library, no Tailwind, no hard-coded color values in components, pair every font size with a line height, use spacing in multiples of 4, and do not put monospace last in the code font stack.
|
||||
|
||||
## 1. 设计 token 表(权威定义)
|
||||
> Status: formerly a “living document” that evolved with `packages/client/web-ui`. The visual baseline came from empirical study of the deepseekchat frontend repository. The [web-styling-system RFC](../.agents/notes/implemented/process/2026-07-19-web-styling-system.md) owns the framework decisions and engineering constraints; this document does not repeat their rationale.
|
||||
|
||||
所有 token 住 `packages/client/web-ui/src/style/global.css`:`:root` 亮色实值,`[data-theme='dark']` 块覆盖同名变量(未补全前列为占位)。组件 CSS 只引 token,不出现字面量色值。
|
||||
## 1. Design token table (authoritative definitions)
|
||||
|
||||
### 1.1 颜色(两层:注释里是 base 色板出处,变量名即语义别名)
|
||||
All tokens live in `packages/client/web-ui/src/style/global.css`: `:root` contains the light-theme values, and the `[data-theme='dark']` block overrides the same variables (columns that were not complete are marked as placeholders). Component CSS references tokens only and contains no literal color values.
|
||||
|
||||
| token | 亮色实值 | 暗色(占位) | 用途 |
|
||||
### 1.1 Colors (two layers: comments identify the base-palette source, while variable names are semantic aliases)
|
||||
|
||||
| token | Light value | Dark value (placeholder) | Purpose |
|
||||
| --- | --- | --- | --- |
|
||||
| `--bg-base` | `#ffffff` | `#151517` | 页面底 |
|
||||
| `--bg-layer` | `#ffffff` | `#232324` | 浮层/面板 |
|
||||
| `--bg-sidebar` | `#f9fafb` | `#1b1b1c` | 侧边栏底 |
|
||||
| `--text-primary` | `#0f1115` | `#f9fafb` | 正文 |
|
||||
| `--text-secondary` | `#61666b` | `#cfd3d6` | 次要文字 |
|
||||
| `--text-tertiary` | `#81858c` | `#adb2b8` | 辅助/说明 |
|
||||
| `--border-l1` | `rgba(0,0,0,.04)` | `rgba(255,255,255,.06)` | 弱分隔(侧边栏右缘) |
|
||||
| `--border-l2` | `rgba(0,0,0,.1)` | `rgba(255,255,255,.12)` | 常规边框 |
|
||||
| `--hover-bg` | `rgba(38,49,72,.06)` | `rgba(255,255,255,.08)` | hover 态底 |
|
||||
| `--active-bg` | `rgba(38,49,72,.1)` | `rgba(255,255,255,.14)` | 按压/激活态底 |
|
||||
| `--accent` | `#3964fe` | `#5686fe` | 品牌蓝(deepseek-500;暗提亮一档) |
|
||||
| `--accent-soft` | `#edf3fe` | `#28313f` | 淡品牌底(强调块) |
|
||||
| `--accent-item` | `#e4edfd` | `#35363a` | 侧边栏选中条目底 |
|
||||
| `--bubble-bg` | `#edf3fe` | `#2c2c2e` | 用户消息气泡底 |
|
||||
| `--ok` / `--error` / `--warn` | `#22c55e` / `#ec1313` / `#f59e0b` | 同值 | 语义状态色 |
|
||||
| `--text-on-solid` | `#ffffff` | 同值 | 实色底(accent/error 徽标等)上的文字 |
|
||||
| `--ok-soft` / `--error-soft` | `#e6faed` / `#fee2e2` | `#233c2c` / `#570c0c` | 语义状态软底(徽章);green-100/red-100,暗为 900 档 |
|
||||
| `--color-frame-mux` / `--color-frame-host` | `#8250df` / `#0969da` | 同值 | RPC 调试面板方向色(自有,非基线) |
|
||||
| `--frame-mux-soft` / `--frame-host-soft` | `rgba(130,80,223,.1)` / `rgba(9,105,218,.1)` | 同色 `.24` | 方向色软底(徽章) |
|
||||
| `--scroll-color` / `--scroll-color-hover` | `rgba(0,0,0,.08)` / `.15` | `rgba(255,255,255,.15)` / `.24` | 滚动条(`.scrollable` 专用) |
|
||||
| `--bg-base` | `#ffffff` | `#151517` | Page background |
|
||||
| `--bg-layer` | `#ffffff` | `#232324` | Floating layer/panel |
|
||||
| `--bg-sidebar` | `#f9fafb` | `#1b1b1c` | Sidebar background |
|
||||
| `--text-primary` | `#0f1115` | `#f9fafb` | Body text |
|
||||
| `--text-secondary` | `#61666b` | `#cfd3d6` | Secondary text |
|
||||
| `--text-tertiary` | `#81858c` | `#adb2b8` | Supporting/descriptive text |
|
||||
| `--border-l1` | `rgba(0,0,0,.04)` | `rgba(255,255,255,.06)` | Subtle separator (sidebar right edge) |
|
||||
| `--border-l2` | `rgba(0,0,0,.1)` | `rgba(255,255,255,.12)` | Standard border |
|
||||
| `--hover-bg` | `rgba(38,49,72,.06)` | `rgba(255,255,255,.08)` | Hover-state background |
|
||||
| `--active-bg` | `rgba(38,49,72,.1)` | `rgba(255,255,255,.14)` | Pressed/active-state background |
|
||||
| `--accent` | `#3964fe` | `#5686fe` | Brand blue (deepseek-500; one step lighter in dark mode) |
|
||||
| `--accent-soft` | `#edf3fe` | `#28313f` | Soft brand background (emphasis blocks) |
|
||||
| `--accent-item` | `#e4edfd` | `#35363a` | Selected sidebar-item background |
|
||||
| `--bubble-bg` | `#edf3fe` | `#2c2c2e` | User-message bubble background |
|
||||
| `--ok` / `--error` / `--warn` | `#22c55e` / `#ec1313` / `#f59e0b` | Same values | Semantic status colors |
|
||||
| `--text-on-solid` | `#ffffff` | Same value | Text on solid backgrounds (accent/error badges, etc.) |
|
||||
| `--ok-soft` / `--error-soft` | `#e6faed` / `#fee2e2` | `#233c2c` / `#570c0c` | Soft semantic status backgrounds (badges); green-100/red-100, with the 900 shades in dark mode |
|
||||
| `--color-frame-mux` / `--color-frame-host` | `#8250df` / `#0969da` | Same values | RPC debugger direction colors (project-specific, not part of the baseline) |
|
||||
| `--frame-mux-soft` / `--frame-host-soft` | `rgba(130,80,223,.1)` / `rgba(9,105,218,.1)` | Same colors at `.24` | Soft direction-color backgrounds (badges) |
|
||||
| `--scroll-color` / `--scroll-color-hover` | `rgba(0,0,0,.08)` / `.15` | `rgba(255,255,255,.15)` / `.24` | Scrollbar colors (for `.scrollable` only) |
|
||||
|
||||
### 1.2 非颜色
|
||||
### 1.2 Non-color tokens
|
||||
|
||||
| token | 值 | 说明 |
|
||||
| token | Value | Description |
|
||||
| --- | --- | --- |
|
||||
| `--font-ui` | `Inter, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif` | 正文栈 |
|
||||
| `--font-mono` | `Menlo, Monaco, Consolas, 'JetBrains Mono', 'Courier New', sans-serif` | 代码栈;**末位不放 monospace**(防 Windows 中文回退宋体) |
|
||||
| `--fw-strong` | `600` | 粗体统一权重 |
|
||||
| `--ease` | `cubic-bezier(.4,0,.2,1)` | 唯一缓动曲线 |
|
||||
| `--dur` / `--dur-fast` / `--dur-slow` | `.2s` / `.1s` / `.3s` | 过渡三档 |
|
||||
| `--radius-s` / `--radius-m` / `--radius-l` / `--radius-bubble` / `--radius-xl` | `8px` / `12px` / `16px` / `22px` / `24px` | 圆角语义档:小控件 / 列表条目与面板内块 / 浮层 / 气泡 / 输入卡片(基线 inputWrapper 同值);胶囊直接写 `999px` |
|
||||
| `--shadow-panel` | `0 0 1px rgba(0,0,0,.2), 0 0 4px rgba(0,0,0,.02), 0 12px 32px rgba(0,0,0,.08)` | 浮层阴影(基线 lv3) |
|
||||
| `--shadow-float` | `0 0 1px rgba(0,0,0,.24), 0 4px 12px rgba(0,0,0,.06), 0 16px 48px rgba(0,0,0,.16)` | 强浮动面板(lv3 加强档,如 RPC 调试浮层) |
|
||||
| `--shadow-card` | `0 4px 10px rgba(0,0,0,.02), 0 2px 4px rgba(0,0,0,.04)`;暗色 `none` | 输入卡片微阴影(基线:亮色同底靠边框+微影区分,暗色靠提亮底、阴影关闭) |
|
||||
| `--font-ui` | `Inter, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif` | Body-text stack |
|
||||
| `--font-mono` | `Menlo, Monaco, Consolas, 'JetBrains Mono', 'Courier New', sans-serif` | Code stack; **do not put monospace last** (prevents SimSun fallback for Chinese on Windows) |
|
||||
| `--fw-strong` | `600` | Unified bold weight |
|
||||
| `--ease` | `cubic-bezier(.4,0,.2,1)` | Sole easing curve |
|
||||
| `--dur` / `--dur-fast` / `--dur-slow` | `.2s` / `.1s` / `.3s` | Three transition durations |
|
||||
| `--radius-s` / `--radius-m` / `--radius-l` / `--radius-bubble` / `--radius-xl` | `8px` / `12px` / `16px` / `22px` / `24px` | Semantic radius steps: small controls / list items and blocks inside panels / floating layers / bubbles / input cards (same as the baseline inputWrapper); use `999px` directly for pills |
|
||||
| `--shadow-panel` | `0 0 1px rgba(0,0,0,.2), 0 0 4px rgba(0,0,0,.02), 0 12px 32px rgba(0,0,0,.08)` | Floating-layer shadow (baseline lv3) |
|
||||
| `--shadow-float` | `0 0 1px rgba(0,0,0,.24), 0 4px 12px rgba(0,0,0,.06), 0 16px 48px rgba(0,0,0,.16)` | Strong floating panel (enhanced lv3, such as the RPC debugger overlay) |
|
||||
| `--shadow-card` | `0 4px 10px rgba(0,0,0,.02), 0 2px 4px rgba(0,0,0,.04)`; dark value `none` | Subtle input-card shadow (baseline: borders plus a subtle shadow distinguish same-color light surfaces; a lighter background distinguishes dark surfaces, with the shadow disabled) |
|
||||
|
||||
字号与间距**不 token 化**(基线仓同款决策):字号在组件里写 px 且**成对写行高**,常用对 16/24(气泡)、14/22(UI 默认)、12/18(辅助);间距用 4 的倍数。
|
||||
Font sizes and spacing are **not tokenized** (matching the baseline repository's decision): components specify font sizes in px and **always pair them with line heights**. Common pairs are 16/24 (bubbles), 14/22 (UI default), and 12/18 (supporting text); spacing uses multiples of 4.
|
||||
|
||||
## 2. 视觉基线(源自 deepseekchat)
|
||||
## 2. Visual baseline (from deepseekchat)
|
||||
|
||||
- 侧边栏:宽 `260px + 1px` 右边框(`--border-l1`);底色 `--bg-sidebar`。
|
||||
- 侧边栏条目:高 `40px`、圆角 `--radius-m`、字号 14px;hover 底 `--hover-bg` 或 sidebar 专属灰、**选中底 `--accent-item` 且不改文字色**。
|
||||
- 侧边栏分组标题:12px / weight 500 / `--text-tertiary` / sticky 顶部(底色同侧边栏遮滚动内容)。
|
||||
- 会话列:`max-width: 840px` 居中,<1024px 降 712px。
|
||||
- 消息流:**仅用户侧有气泡**——`--bubble-bg` 底、圆角 `--radius-bubble`、padding `10px 16px`、字号 16px/24px、`max-width: calc(100% - 88px)`;**助手侧纯文档流无底色**。
|
||||
- 消息操作条:默认 `opacity: 0`,父块 hover/focus-within 淡入(`--dur` + `--ease`)。
|
||||
- 输入卡片:与会话列同宽(840px,<1024px 降 712px)居中悬浮(距底留白带);圆角 `--radius-xl`、边框 `--border-l2`、底 `--bg-base`、阴影 `--shadow-card`;内部上下两段=textarea(16px/24px,min 2 行 max 14 行=336px,镜像 div 自增高)+ 操作行(右下嵌 34px 主圆钮);focus 无边框/阴影变化(基线同款)。
|
||||
- 输入主按钮(拍板 2026-07-20 三连,视觉参照 Codex App):32px 实心正圆图标钮(内联 SVG)——空闲=`--accent` 底白↑箭头「发送」,运行中原地变 `--accent-soft` 底 accent ■「停止」(同色系不告警、不用红)。**运行中锁输入**(拍板 3,取代早先 hover 菜单方案):textarea disabled(灰、草稿内容保留可见)、无任何排队/插话菜单,停止是唯一动作;turn 结束解禁并 refocus。键盘 Enter=发送、Ctrl/Meta+Enter=换行(运行中键盘路径随锁失效)。
|
||||
- 滚动条:近隐形、hover 加深、`scrollbar-gutter: stable` 不占布局(统一走 `.scrollable`,见 §3-9)。
|
||||
- RPC 四象限方向符(官方视觉词汇,空间隐喻:上=去 server、下=来自 server;单线=unary、双线=SSE):
|
||||
- Sidebar: width `260px + 1px` right border (`--border-l1`); background `--bg-sidebar`.
|
||||
- Sidebar items: height `40px`, radius `--radius-m`, font size 14px; hover background `--hover-bg` or a sidebar-specific gray; **selected items use `--accent-item` without changing text color**.
|
||||
- Sidebar group headings: 12px / weight 500 / `--text-tertiary` / sticky at the top (using the sidebar background to cover scrolling content).
|
||||
- Conversation column: centered at `max-width: 840px`, reduced to 712px below 1024px.
|
||||
- Message stream: **only user messages have bubbles**: background `--bubble-bg`, radius `--radius-bubble`, padding `10px 16px`, font size 16px/24px, and `max-width: calc(100% - 88px)`; **assistant messages are a plain document flow without a background**.
|
||||
- Message action bar: `opacity: 0` by default; fades in when its parent is hovered or contains focus (`--dur` + `--ease`).
|
||||
- Input card: floats centered at the same width as the conversation column (840px, reduced to 712px below 1024px) with bottom spacing; radius `--radius-xl`, border `--border-l2`, background `--bg-base`, shadow `--shadow-card`; two internal vertical sections = textarea (16px/24px, minimum 2 lines, maximum 14 lines = 336px, auto-growing through a mirror div) + action row (a 34px primary round button nested at bottom right); focus does not change the border or shadow (matching the baseline).
|
||||
- Primary input button (the three decisions made on 2026-07-20, visually based on the Codex App): a 32px solid circular icon button (inline SVG). Idle = `--accent` background with a white ↑ “Send” arrow; while running it changes in place to an accent ■ “Stop” icon on `--accent-soft` (the same color family, not a warning, and not red). **Input is locked while running** (decision 3, replacing the earlier hover-menu design): the textarea is disabled (gray, with draft content still visible), there is no queue/interjection menu, and Stop is the only action. When the turn ends, input unlocks and regains focus. Enter sends; Ctrl/Meta+Enter inserts a newline (the keyboard path is disabled with the locked input while running).
|
||||
- Scrollbars: nearly invisible, darkening on hover, with `scrollbar-gutter: stable` so they do not consume layout space (always use `.scrollable`; see § 3.9).
|
||||
- Four-quadrant RPC direction symbols (the official visual vocabulary, using the spatial metaphor that up goes to the server, down comes from the server; single line = unary, double line = SSE):
|
||||
|
||||
| 符号 | 象限 | 徽章配色 |
|
||||
| Symbol | Quadrant | Badge colors |
|
||||
| --- | --- | --- |
|
||||
| `↑` | client-request(unary 出站) | `--accent` / `--accent-soft` |
|
||||
| `↓` | server-response(unary 回包) | ok `--ok`/`--ok-soft`,error `--error`/`--error-soft` |
|
||||
| `⇟` | server-request(SSE 帧推送) | mux `--color-frame-mux`/`--frame-mux-soft`,host `--color-frame-host`/`--frame-host-soft` |
|
||||
| `⇞` | client-response(SSE 侧回应) | `--accent`/`--accent-soft` 降透明度 |
|
||||
| `↑` | client-request (unary outbound) | `--accent` / `--accent-soft` |
|
||||
| `↓` | server-response (unary response) | ok `--ok`/`--ok-soft`, error `--error`/`--error-soft` |
|
||||
| `⇟` | server-request (SSE frame push) | mux `--color-frame-mux`/`--frame-mux-soft`, host `--color-frame-host`/`--frame-host-soft` |
|
||||
| `⇞` | client-response (SSE-side response) | `--accent`/`--accent-soft` at reduced opacity |
|
||||
|
||||
## 3. 样式编码规范(review 对照打勾)
|
||||
## 3. Style implementation rules (review checklist)
|
||||
|
||||
1. 颜色/圆角/动效/字体栈只引 §1 token;组件 CSS 出现字面量色值即打回(渐变遮罩等特效除外,须注释说明)。
|
||||
2. 组件 CSS 禁止出现 `[data-theme]` 选择器;暗色差异只在 global.css token 表做。确需按主题换非 token 值(渐变端点等),组件定义局部 CSS 变量、主题块只覆写变量(变量桥)。
|
||||
3. 类名 camelCase;状态类用单形容词(`.active` `.show`),由 clsx 挂载:`clsx(styles.x, cond && styles.active, className)`。
|
||||
4. 对外组件必须透传 `className` 并合入根元素。
|
||||
5. 禁用 `composes`;复用靠 token 与组件抽取。
|
||||
6. `:global` 仅用于穿透第三方/跨包类名;禁止用它定义新全局类。
|
||||
7. 交互过渡一律 `var(--dur*) var(--ease)`,只过渡 opacity / transform / 背景色 / 阴影;纯 hover 展示型元素包 `@media (hover: hover)`。
|
||||
8. hover/active 底色优先用透明度制 token(叠任意海拔底色都成立),不新造实色灰。
|
||||
9. 滚动容器统一挂 global.css 的 `.scrollable` 工具类;组件内禁写 `::-webkit-scrollbar`。
|
||||
10. 媒体查询写在组件 css 尾部、贴着被覆盖规则;断点当前仅 1024px 一档(会话列降档),加第二档需先记入本文档。
|
||||
11. 动态样式 JS 侧只写 CSS 变量(`style={{'--x': v}}`),规则留在 CSS;禁止在 TSX 里拼接样式对象做主题/状态分支。
|
||||
12. 文字灰阶只用 `--text-primary/secondary/tertiary` 三级,不新造灰色。
|
||||
1. Colors, radii, motion, and font stacks reference only the § 1 tokens. Reject literal color values in component CSS (except for special effects such as gradient masks, which require an explanatory comment).
|
||||
2. Component CSS must not contain `[data-theme]` selectors; dark-mode differences belong only in the global.css token table. If a theme must change a non-token value such as a gradient endpoint, define a local CSS variable in the component and have the theme block override only that variable (a variable bridge).
|
||||
3. Use camelCase class names; use a single adjective for state classes (`.active` `.show`) and attach them with clsx: `clsx(styles.x, cond && styles.active, className)`.
|
||||
4. Public components must accept `className` and merge it into the root element.
|
||||
5. Do not use `composes`; share through tokens and extracted components.
|
||||
6. Use `:global` only to pierce third-party or cross-package class names; do not use it to define new global classes.
|
||||
7. All interaction transitions use `var(--dur*) var(--ease)` and transition only opacity / transform / background color / shadow. Wrap hover-only reveal elements in `@media (hover: hover)`.
|
||||
8. Prefer opacity-based tokens for hover/active backgrounds because they compose over any elevation background; do not add new solid grays.
|
||||
9. Apply the `.scrollable` utility class from global.css to every scroll container; do not write `::-webkit-scrollbar` inside components.
|
||||
10. Put media queries at the end of the component CSS, next to the rules they override. The only current breakpoint is 1024px (where the conversation column steps down); record a second breakpoint in this document before adding it.
|
||||
11. Dynamic styles in JS set only CSS variables (`style={{'--x': v}}`), while rules remain in CSS; do not assemble style objects in TSX to branch by theme or state.
|
||||
12. Use only the three `--text-primary/secondary/tertiary` levels for gray text; do not add another gray.
|
||||
|
||||
## 4. 文件组织
|
||||
## 4. File organization
|
||||
|
||||
- `src/style/global.css` 固定分区顺序:① token 表(`:root` + `[data-theme='dark']`)② 全局基础(box-sizing、body、button reset)③ 全局工具类(`.scrollable` 等,总数保持个位数)。
|
||||
- `*.module.css` 与组件同目录同名;一个组件一个 module 文件。
|
||||
- 类型声明用现有 `css-modules.d.ts` 通配;组件数超 20 再评估引入 tcm 生成精确 `.css.d.ts`。
|
||||
- PostCSS 特性白名单:当前**零插件**(平铺 CSS + 原生嵌套按需);引入 nested/custom-media 需先记入本文档。
|
||||
- `src/style/global.css` always uses this section order: ① token table (`:root` + `[data-theme='dark']`), ② global foundations (box-sizing, body, button reset), ③ global utility classes (`.scrollable`, etc.; keep the total in single digits).
|
||||
- Place each `*.module.css` beside the component with the same name; use one module file per component.
|
||||
- Use the existing `css-modules.d.ts` wildcard declaration. Reassess introducing tcm to generate exact `.css.d.ts` files only after the component count exceeds 20.
|
||||
- PostCSS feature allowlist: currently **no plugins** (flat CSS plus native nesting when needed). Record nested/custom-media in this document before introducing either.
|
||||
|
||||
## 5. 演进规则与偏离记录
|
||||
## 5. Evolution rules and deviation log
|
||||
|
||||
- **加新 token**:先进 §1 表(含暗色占位列)再在组件使用;review 见到未入表的 `--` 新变量即打回(组件局部变量桥除外)。
|
||||
- **偏离基线**:与 §2 任一常数不一致的实现,须在下方偏离表记一行(日期/项/理由)。
|
||||
- **暗色表补全验收**:`[data-theme='dark']` 覆盖 §1 全部占位列后,用 RPC 面板 + 侧边栏 + 会话流三个界面人工/截图核对一遍,无组件级主题选择器即达标。
|
||||
- **Adding a token**: add it to the § 1 table first (including the dark-placeholder column), then use it in the component. Reject any new `--` variable that has not been added to the table (except for component-local variable bridges).
|
||||
- **Deviating from the baseline**: if an implementation differs from any constant in § 2, add one row to the deviation table below (date / item / rationale).
|
||||
- **Dark-table completion acceptance**: after `[data-theme='dark']` overrides every placeholder column in § 1, compare the RPC panel, sidebar, and conversation stream manually or with screenshots. Acceptance requires all three to match and no component-level theme selector to remain.
|
||||
|
||||
| 日期 | 偏离项 | 理由 |
|
||||
| Date | Deviation | Rationale |
|
||||
| --- | --- | --- |
|
||||
| (空) | | |
|
||||
| (none) | | |
|
||||
|
||||
## 6. 相关文档
|
||||
## 6. Related documentation
|
||||
|
||||
- [web-styling-system RFC](../.agents/notes/implemented/process/2026-07-19-web-styling-system.md)(框架五条与工程约束的裁决记录)
|
||||
- 客户端消费架构与分层协议:[Web 客户端架构 RFC](../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md)、[GUI 分层与 RPC 协议 RFC](../.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md)
|
||||
- [web-styling-system RFC](../.agents/notes/implemented/process/2026-07-19-web-styling-system.md) (decision record for the five framework rules and engineering constraints)
|
||||
- Client consumption architecture and layered protocols: [Web client architecture RFC](../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md), [GUI layering and RPC protocol RFC](../.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md)
|
||||
|
||||
109
docs/web-styling.zh.md
Normal file
109
docs/web-styling.zh.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Web GUI 样式规范
|
||||
|
||||
[English](web-styling.md) | 中文
|
||||
|
||||
> **【token 体系已换代——§1 表格仅历史参考】** 本文的 `--bg-*`/`--text-*`/`--accent` token 族与其宿主包 `packages/client/web-ui` 已随插件化重构退役。现行 token 唯一来源=`packages/client/ui-theme/src/styles/` 的 `--dsw-*` 体系(static 色阶+alias 语义层,暗色=`body[data-ds-dark-theme]` 覆写),sheet 即权威、组件对账以它为准。**仍然有效**:工程约束(CSS Modules + clsx、无组件库、无 tailwind、组件禁 hardcode 色值)、字号成对写行高、间距 4 倍数、代码字体栈末位不放 monospace。
|
||||
|
||||
> 状态:原「活文档」(随 `packages/client/web-ui` 演进)。视觉基线源自对 deepseekchat 前端仓的实测调研。框架决策与工程约束由 [web-styling-system RFC](../.agents/notes/implemented/process/2026-07-19-web-styling-system.md) 拍板,本文不重复论证。
|
||||
|
||||
## 1. 设计 token 表(权威定义)
|
||||
|
||||
所有 token 住 `packages/client/web-ui/src/style/global.css`:`:root` 亮色实值,`[data-theme='dark']` 块覆盖同名变量(未补全前列为占位)。组件 CSS 只引 token,不出现字面量色值。
|
||||
|
||||
### 1.1 颜色(两层:注释里是 base 色板出处,变量名即语义别名)
|
||||
|
||||
| token | 亮色实值 | 暗色(占位) | 用途 |
|
||||
| --- | --- | --- | --- |
|
||||
| `--bg-base` | `#ffffff` | `#151517` | 页面底 |
|
||||
| `--bg-layer` | `#ffffff` | `#232324` | 浮层/面板 |
|
||||
| `--bg-sidebar` | `#f9fafb` | `#1b1b1c` | 侧边栏底 |
|
||||
| `--text-primary` | `#0f1115` | `#f9fafb` | 正文 |
|
||||
| `--text-secondary` | `#61666b` | `#cfd3d6` | 次要文字 |
|
||||
| `--text-tertiary` | `#81858c` | `#adb2b8` | 辅助/说明 |
|
||||
| `--border-l1` | `rgba(0,0,0,.04)` | `rgba(255,255,255,.06)` | 弱分隔(侧边栏右缘) |
|
||||
| `--border-l2` | `rgba(0,0,0,.1)` | `rgba(255,255,255,.12)` | 常规边框 |
|
||||
| `--hover-bg` | `rgba(38,49,72,.06)` | `rgba(255,255,255,.08)` | hover 态底 |
|
||||
| `--active-bg` | `rgba(38,49,72,.1)` | `rgba(255,255,255,.14)` | 按压/激活态底 |
|
||||
| `--accent` | `#3964fe` | `#5686fe` | 品牌蓝(deepseek-500;暗提亮一档) |
|
||||
| `--accent-soft` | `#edf3fe` | `#28313f` | 淡品牌底(强调块) |
|
||||
| `--accent-item` | `#e4edfd` | `#35363a` | 侧边栏选中条目底 |
|
||||
| `--bubble-bg` | `#edf3fe` | `#2c2c2e` | 用户消息气泡底 |
|
||||
| `--ok` / `--error` / `--warn` | `#22c55e` / `#ec1313` / `#f59e0b` | 同值 | 语义状态色 |
|
||||
| `--text-on-solid` | `#ffffff` | 同值 | 实色底(accent/error 徽标等)上的文字 |
|
||||
| `--ok-soft` / `--error-soft` | `#e6faed` / `#fee2e2` | `#233c2c` / `#570c0c` | 语义状态软底(徽章);green-100/red-100,暗为 900 档 |
|
||||
| `--color-frame-mux` / `--color-frame-host` | `#8250df` / `#0969da` | 同值 | RPC 调试面板方向色(自有,非基线) |
|
||||
| `--frame-mux-soft` / `--frame-host-soft` | `rgba(130,80,223,.1)` / `rgba(9,105,218,.1)` | 同色 `.24` | 方向色软底(徽章) |
|
||||
| `--scroll-color` / `--scroll-color-hover` | `rgba(0,0,0,.08)` / `.15` | `rgba(255,255,255,.15)` / `.24` | 滚动条(`.scrollable` 专用) |
|
||||
|
||||
### 1.2 非颜色
|
||||
|
||||
| token | 值 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `--font-ui` | `Inter, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif` | 正文栈 |
|
||||
| `--font-mono` | `Menlo, Monaco, Consolas, 'JetBrains Mono', 'Courier New', sans-serif` | 代码栈;**末位不放 monospace**(防 Windows 中文回退宋体) |
|
||||
| `--fw-strong` | `600` | 粗体统一权重 |
|
||||
| `--ease` | `cubic-bezier(.4,0,.2,1)` | 唯一缓动曲线 |
|
||||
| `--dur` / `--dur-fast` / `--dur-slow` | `.2s` / `.1s` / `.3s` | 过渡三档 |
|
||||
| `--radius-s` / `--radius-m` / `--radius-l` / `--radius-bubble` / `--radius-xl` | `8px` / `12px` / `16px` / `22px` / `24px` | 圆角语义档:小控件 / 列表条目与面板内块 / 浮层 / 气泡 / 输入卡片(基线 inputWrapper 同值);胶囊直接写 `999px` |
|
||||
| `--shadow-panel` | `0 0 1px rgba(0,0,0,.2), 0 0 4px rgba(0,0,0,.02), 0 12px 32px rgba(0,0,0,.08)` | 浮层阴影(基线 lv3) |
|
||||
| `--shadow-float` | `0 0 1px rgba(0,0,0,.24), 0 4px 12px rgba(0,0,0,.06), 0 16px 48px rgba(0,0,0,.16)` | 强浮动面板(lv3 加强档,如 RPC 调试浮层) |
|
||||
| `--shadow-card` | `0 4px 10px rgba(0,0,0,.02), 0 2px 4px rgba(0,0,0,.04)`;暗色 `none` | 输入卡片微阴影(基线:亮色同底靠边框+微影区分,暗色靠提亮底、阴影关闭) |
|
||||
|
||||
字号与间距**不 token 化**(基线仓同款决策):字号在组件里写 px 且**成对写行高**,常用对 16/24(气泡)、14/22(UI 默认)、12/18(辅助);间距用 4 的倍数。
|
||||
|
||||
## 2. 视觉基线(源自 deepseekchat)
|
||||
|
||||
- 侧边栏:宽 `260px + 1px` 右边框(`--border-l1`);底色 `--bg-sidebar`。
|
||||
- 侧边栏条目:高 `40px`、圆角 `--radius-m`、字号 14px;hover 底 `--hover-bg` 或 sidebar 专属灰、**选中底 `--accent-item` 且不改文字色**。
|
||||
- 侧边栏分组标题:12px / weight 500 / `--text-tertiary` / sticky 顶部(底色同侧边栏遮滚动内容)。
|
||||
- 会话列:`max-width: 840px` 居中,<1024px 降 712px。
|
||||
- 消息流:**仅用户侧有气泡**——`--bubble-bg` 底、圆角 `--radius-bubble`、padding `10px 16px`、字号 16px/24px、`max-width: calc(100% - 88px)`;**助手侧纯文档流无底色**。
|
||||
- 消息操作条:默认 `opacity: 0`,父块 hover/focus-within 淡入(`--dur` + `--ease`)。
|
||||
- 输入卡片:与会话列同宽(840px,<1024px 降 712px)居中悬浮(距底留白带);圆角 `--radius-xl`、边框 `--border-l2`、底 `--bg-base`、阴影 `--shadow-card`;内部上下两段=textarea(16px/24px,min 2 行 max 14 行=336px,镜像 div 自增高)+ 操作行(右下嵌 34px 主圆钮);focus 无边框/阴影变化(基线同款)。
|
||||
- 输入主按钮(拍板 2026-07-20 三连,视觉参照 Codex App):32px 实心正圆图标钮(内联 SVG)——空闲=`--accent` 底白↑箭头「发送」,运行中原地变 `--accent-soft` 底 accent ■「停止」(同色系不告警、不用红)。**运行中锁输入**(拍板 3,取代早先 hover 菜单方案):textarea disabled(灰、草稿内容保留可见)、无任何排队/插话菜单,停止是唯一动作;turn 结束解禁并 refocus。键盘 Enter=发送、Ctrl/Meta+Enter=换行(运行中键盘路径随锁失效)。
|
||||
- 滚动条:近隐形、hover 加深、`scrollbar-gutter: stable` 不占布局(统一走 `.scrollable`,见 §3-9)。
|
||||
- RPC 四象限方向符(官方视觉词汇,空间隐喻:上=去 server、下=来自 server;单线=unary、双线=SSE):
|
||||
|
||||
| 符号 | 象限 | 徽章配色 |
|
||||
| --- | --- | --- |
|
||||
| `↑` | client-request(unary 出站) | `--accent` / `--accent-soft` |
|
||||
| `↓` | server-response(unary 回包) | ok `--ok`/`--ok-soft`,error `--error`/`--error-soft` |
|
||||
| `⇟` | server-request(SSE 帧推送) | mux `--color-frame-mux`/`--frame-mux-soft`,host `--color-frame-host`/`--frame-host-soft` |
|
||||
| `⇞` | client-response(SSE 侧回应) | `--accent`/`--accent-soft` 降透明度 |
|
||||
|
||||
## 3. 样式编码规范(review 对照打勾)
|
||||
|
||||
1. 颜色/圆角/动效/字体栈只引 §1 token;组件 CSS 出现字面量色值即打回(渐变遮罩等特效除外,须注释说明)。
|
||||
2. 组件 CSS 禁止出现 `[data-theme]` 选择器;暗色差异只在 global.css token 表做。确需按主题换非 token 值(渐变端点等),组件定义局部 CSS 变量、主题块只覆写变量(变量桥)。
|
||||
3. 类名 camelCase;状态类用单形容词(`.active` `.show`),由 clsx 挂载:`clsx(styles.x, cond && styles.active, className)`。
|
||||
4. 对外组件必须透传 `className` 并合入根元素。
|
||||
5. 禁用 `composes`;复用靠 token 与组件抽取。
|
||||
6. `:global` 仅用于穿透第三方/跨包类名;禁止用它定义新全局类。
|
||||
7. 交互过渡一律 `var(--dur*) var(--ease)`,只过渡 opacity / transform / 背景色 / 阴影;纯 hover 展示型元素包 `@media (hover: hover)`。
|
||||
8. hover/active 底色优先用透明度制 token(叠任意海拔底色都成立),不新造实色灰。
|
||||
9. 滚动容器统一挂 global.css 的 `.scrollable` 工具类;组件内禁写 `::-webkit-scrollbar`。
|
||||
10. 媒体查询写在组件 css 尾部、贴着被覆盖规则;断点当前仅 1024px 一档(会话列降档),加第二档需先记入本文档。
|
||||
11. 动态样式 JS 侧只写 CSS 变量(`style={{'--x': v}}`),规则留在 CSS;禁止在 TSX 里拼接样式对象做主题/状态分支。
|
||||
12. 文字灰阶只用 `--text-primary/secondary/tertiary` 三级,不新造灰色。
|
||||
|
||||
## 4. 文件组织
|
||||
|
||||
- `src/style/global.css` 固定分区顺序:① token 表(`:root` + `[data-theme='dark']`)② 全局基础(box-sizing、body、button reset)③ 全局工具类(`.scrollable` 等,总数保持个位数)。
|
||||
- `*.module.css` 与组件同目录同名;一个组件一个 module 文件。
|
||||
- 类型声明用现有 `css-modules.d.ts` 通配;组件数超 20 再评估引入 tcm 生成精确 `.css.d.ts`。
|
||||
- PostCSS 特性白名单:当前**零插件**(平铺 CSS + 原生嵌套按需);引入 nested/custom-media 需先记入本文档。
|
||||
|
||||
## 5. 演进规则与偏离记录
|
||||
|
||||
- **加新 token**:先进 §1 表(含暗色占位列)再在组件使用;review 见到未入表的 `--` 新变量即打回(组件局部变量桥除外)。
|
||||
- **偏离基线**:与 §2 任一常数不一致的实现,须在下方偏离表记一行(日期/项/理由)。
|
||||
- **暗色表补全验收**:`[data-theme='dark']` 覆盖 §1 全部占位列后,用 RPC 面板 + 侧边栏 + 会话流三个界面人工/截图核对一遍,无组件级主题选择器即达标。
|
||||
|
||||
| 日期 | 偏离项 | 理由 |
|
||||
| --- | --- | --- |
|
||||
| (空) | | |
|
||||
|
||||
## 6. 相关文档
|
||||
|
||||
- [web-styling-system RFC](../.agents/notes/implemented/process/2026-07-19-web-styling-system.md)(框架五条与工程约束的裁决记录)
|
||||
- 客户端消费架构与分层协议:[Web 客户端架构 RFC](../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md)、[GUI 分层与 RPC 协议 RFC](../.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md)
|
||||
Reference in New Issue
Block a user