mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
docs(i18n): proofread README translations 61-80
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
只包含 bin 的应用,启动外部 `cordis.yml`;其 [`jsonrpc`](../../ui/jsonrpc/README.md) 入口通过按换行分隔的 stdio 为 SDK 客户端提供服务。配置负责组合主干、后端和服务插件。`lib/bin.js` 也是[单文件可执行 runtime](../../../.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md) 的入口。
|
||||
只包含 bin 的应用,启动外部 `cordis.yml`;其 [`jsonrpc`](../../ui/jsonrpc/README.md) 入口通过按换行分隔的 stdio 为 SDK 客户端提供服务。配置负责组合主干、后端和服务插件。`lib/bin.js` 也是[单文件可执行运行时](../../../.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md) 的入口。
|
||||
|
||||
## 配置发现
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
## 退出生命周期
|
||||
|
||||
stdin EOF 和 `SIGTERM` 会将根上下文释放至静默并以 0 退出;`SIGINT` 完成同样的释放后以 130 退出。EOF 可能按[分发 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md) 所述截断正在处理的轮次。`jsonrpc` 插件拥有先响应再退出的协议关闭流程;两条路径均幂等,可以安全竞态。
|
||||
stdin EOF 和 `SIGTERM` 会 dispose(释放资源)根上下文,等待完全停稳后以 0 退出;`SIGINT` 完成同样的 dispose 后以 130 退出。EOF 可能按[分发 Agent Note(agent 决策记录)](../../../.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md) 所述截断正在处理的轮次。`jsonrpc` 插件拥有先响应再退出的协议关闭流程;两条路径均幂等,即使发生竞态也安全。
|
||||
|
||||
## stdout 是协议
|
||||
|
||||
@@ -20,14 +20,14 @@ stdout 只承载 JSON-RPC 帧。bin 和启动守卫在 stderr 上输出诊断,
|
||||
|
||||
## 模型体验
|
||||
|
||||
模型通过外部 `cordis.yml` 加载的插件间接获得体验;每个插件拥有自身面向模型的提示词、schema、消息和结果,此 bin 不添加任何内容。
|
||||
模型体验由外部 `cordis.yml` 加载的插件间接提供;这些插件负责所有面向模型的提示词、schema、消息和结果,此 bin 不添加任何内容。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
不会直接失效;具名消费方拥有请求前缀的任何变更。
|
||||
不会直接失效;由上述消费方负责请求前缀的任何变更。
|
||||
|
||||
## 已知限制与延后工作
|
||||
|
||||
- **bin 无法证明配置提供 JSON-RPC 服务**:不含 `dsh-jsonrpc` 条目的有效配置也能成功启动,但不会提供任何服务。
|
||||
- **不存在内置或默认配置**:每次启动都必须提供 `DSH_CORDIS_CONFIG` 或位置路径;部署拥有完整插件树和 stdout 纪律。
|
||||
- **不存在内置或默认配置**:每次启动都必须提供 `DSH_CORDIS_CONFIG` 或位置路径;部署方负责完整的插件树和 stdout 纪律。
|
||||
- **stdin EOF 会截断正在处理的工作**:客户端消失时立即释放根上下文;需要有序完成的调用方应使用协议级 `shutdown` 请求。
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
全屏终端应用组合包:一个 Cordis 插件,组合 [`@deepseek-ai/dsh-agent-spine-demo`](../agent-spine-demo/README.md)、持久的同会话目标、人类命令注册表与 `/goal` 生产方、JSONL 持久化、键盘支持的用户交互、预创建的 `main` agent,以及 [`@deepseek-ai/dsh-tui`](../../ui/tui/README.md)。一份 `cordis.yml` 将它作为单个条目挂载;[`dsh`](../../../apps/cli/README.md) CLI 是启动此类配置的前端入口。
|
||||
全屏终端应用组合包:一个 Cordis 插件,组合 [`@deepseek-ai/dsh-agent-spine-demo`](../agent-spine-demo/README.md)、持久化的同会话目标、人类命令注册表与 `/goal` 生产方、JSONL 持久化、键盘支持的用户交互、预创建的 `main` agent(智能体),以及 [`@deepseek-ai/dsh-tui`](../../ui/tui/README.md)。一份 `cordis.yml` 将它作为一个 Cordis 配置项挂载;[`dsh`](../../../apps/cli/README.md) CLI(命令行界面)是启动此类配置的入口。
|
||||
|
||||
管道、脚本和其他非交互式运行应使用 [`@deepseek-ai/dsh-cli-demo`](../cli-demo/README.md)。此组合包需要一对 TTY,不提供面向行的回退。
|
||||
|
||||
@@ -11,16 +11,16 @@
|
||||
| 插件 | 设置在此处的原因 |
|
||||
|---|---|
|
||||
| `@deepseek-ai/dsh-agent-spine-demo` | 共享服务、面向模型的工具,以及一个已配置的 `main` agent |
|
||||
| `@deepseek-ai/dsh-commands` | 供 TUI 和命令插件消费的纯人类命令发现与分发 |
|
||||
| `@deepseek-ai/dsh-command-goal` | 直接在主干的持久目标栈上提供 `/goal` 状态与变更 |
|
||||
| `@deepseek-ai/dsh-commands` | 供 TUI 和命令插件消费、仅面向人类的命令发现与分发 |
|
||||
| `@deepseek-ai/dsh-command-goal` | 直接在主干的持久化目标栈上提供 `/goal` 状态与变更 |
|
||||
| `@deepseek-ai/dsh-session-persistence-jsonl` | 位于 `persistenceRoot` 下的持久会话日志 |
|
||||
| `@deepseek-ai/dsh-session-checkpoint-policy` | 模型请求和顶层工具 effect 前的语义持久性屏障,以及已完成步骤的检查点 |
|
||||
| `@deepseek-ai/dsh-session-query-sqlite` + `@deepseek-ai/dsh-session-reference` | TUI 消费的组合式精确/FTS 会话查询与有界 `@session` 快照;面向模型的查询工具仍由叶节点选用 |
|
||||
| `@deepseek-ai/dsh-user-interaction` | 与提供方无关的人类问题服务 |
|
||||
| `@deepseek-ai/dsh-tui` | 全屏记录、编辑器、工具卡片、计划与问题 overlay |
|
||||
| `@deepseek-ai/dsh-tui` | 全屏 transcript(文本记录)、编辑器、工具卡片、计划与问题 overlay |
|
||||
| `@deepseek-ai/dsh-tool-ask-user` | 面向模型的 `ask_user_question` 工具 |
|
||||
|
||||
可替换的 LLM、bash、文件系统和其他能力提供方仍留在叶节点配置中。`@cordisjs/plugin-hmr` 也仍是仅叶节点使用的开发条目,因为它需要 Loader 内部表层。
|
||||
可替换的 LLM(大语言模型)、bash、文件系统和其他能力提供方仍留在叶节点配置中。`@cordisjs/plugin-hmr` 也仍是仅叶节点使用的开发条目,因为它需要 Loader 内部实现。
|
||||
|
||||
## 配置
|
||||
|
||||
@@ -31,27 +31,27 @@
|
||||
| `maxParallelToolCalls` | agent-loop 默认值 | 组合包内循环的并发上限 |
|
||||
| `persona` | 无 | 系统提示词 persona 模板 |
|
||||
| `toolOrder` | 字典序 | 显式的面向模型工具顺序 |
|
||||
| `tools` | 拥有者默认值 | 工具呈现 mode |
|
||||
| `dshHome` | 拥有者默认值 | bash 与 skill 使用的 Harness 主目录 |
|
||||
| `tools` | 拥有者默认值 | 工具呈现模式 |
|
||||
| `dshHome` | 拥有者默认值 | bash 与 skill(技能)使用的 harness 主目录 |
|
||||
| `sessionTitle` | 主干示例限制 | 后备标题词数/字节限制 |
|
||||
| `skills` | 拥有者默认值 | Skill 注册表、本地提供方和工具配置 |
|
||||
| `skills` | 拥有者默认值 | skill 注册表、本地提供方和工具配置 |
|
||||
| `toolBash` | 拥有者默认值 | 面向模型的 bash 工具配置 |
|
||||
| `toolTasks` | 拥有者默认值 | 后台任务控制工具配置,或 `false` |
|
||||
| `goals` | 拥有者默认值 | 持久目标领域与模型工具配置;`false` 会移除目标栈与 `/goal` 生产方 |
|
||||
| `workspaceContext` | 必填 | Workspace 指令配置,或 `false` |
|
||||
| `persistenceRoot` | `./.sessions` | JSONL 持久化根目录,以及派生 `session-query.db` 索引的父目录 |
|
||||
| `persistenceCompression` | `'zstd'` | JSONL 工件编码(`'zstd'` 或原始 `'none'`) |
|
||||
| `persistenceCompression` | `'zstd'` | JSONL 产物编码(`'zstd'` 或原始 `'none'`) |
|
||||
| `sessionReferences` | 服务默认值 | 路由到 `dsh-session-reference` 的跨会话候选项与快照限制 |
|
||||
| `welcome` | `ready.` | TUI 副标题 |
|
||||
| `resumeCommand` | 无 | 退出和无宿主回退的命令模板;选择器本身使用会话查询与宿主移交 |
|
||||
| `ui` | 拥有者默认值 | 推理、颜色、卡片高度等 TUI 呈现设置 |
|
||||
| `ui` | 拥有者默认值 | 推理(reasoning)、颜色、卡片高度等 TUI 呈现设置 |
|
||||
| `resumeSessionId` | 无 | 要恢复的确切持久化会话 |
|
||||
|
||||
新运行会创建 `main-session-<uuid>` 会话 id,并将它同时传给 TUI 与已配置的 agent。恢复运行会将两个组件都绑定到 `resumeSessionId`。TUI 先于主干挂载,因此它可以渲染匹配的配置启动失败,而不会留下空白终端。应用为 `/resume` 组合持久化和会话查询;嵌入宿主还可以提供 `tuiResumeHost`,以原地移交进程。
|
||||
新运行会创建 `main-session-<uuid>` 会话 id,并将它同时传给 TUI 与已配置的 agent。恢复运行会将两个组件都绑定到 `resumeSessionId`。TUI 先于主干挂载,因此它可以渲染匹配的配置启动失败,而不会留下空白终端。应用为 `/resume` 组合持久化和会话查询;嵌入宿主还可以提供 `tuiResumeHost`,用于原地移交进程。
|
||||
|
||||
## 前端入口
|
||||
## 入口
|
||||
|
||||
此包不交付 bin。[`dsh`](../../../apps/cli/README.md) CLI 是终端前端入口:裸 `dsh` 启动已交付的 `examples/tui-agent/cordis.yml`(它挂载此组合包),而 `dsh --config <path-to-cordis.yml>` 启动另一个挂载此组合包的叶节点配置。它加载 cwd 下可选的 `.env`,驱动 Cordis Loader,并等待完整插件树。仓库安装了 Loader 的可选原生辅助程序,因此裸包说明符可以在纯 Node 下解析。
|
||||
此包(package)不交付 bin。[`dsh`](../../../apps/cli/README.md) CLI 是终端入口:裸 `dsh` 启动已交付的 `examples/tui-agent/cordis.yml`(它挂载此组合包),而 `dsh --config <path-to-cordis.yml>` 启动另一个挂载此组合包的叶节点配置。它加载 cwd 下可选的 `.env`,驱动 Cordis Loader,并等待完整插件树。仓库安装了 Loader 的可选原生辅助程序,因此裸包说明符可以在纯 Node 下解析。
|
||||
|
||||
## 叶节点示例
|
||||
|
||||
@@ -80,15 +80,15 @@
|
||||
|
||||
#### 模型所见
|
||||
|
||||
每次非空、非命令的编辑器提交都会成为用户消息;运行中轮次内的提交成为 steering。斜杠命令输入和输出仍只面向人类,而已接受的 `/goal` 变更会追加领域拥有的模型可见状态。共享主干提供已配置的 persona、workspace 指令、skill 目录、目标控制和可见工具 schema。TUI 渲染本身对模型不可见。
|
||||
每次非空、非命令的编辑器提交都会成为用户消息;运行中轮次内的提交成为 steering(中途引导)。斜杠命令输入和输出仍只面向人类,而已接受的 `/goal` 变更会追加领域拥有的模型可见状态。共享主干提供已配置的 persona、workspace 指令、skill 目录、目标控制和可见工具 schema。TUI 渲染本身对模型不可见。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
用户、assistant 与工具历史按常规会话和压缩规则增长。Header、卡片、计划、Markdown 样式和快捷键不增加 token。
|
||||
用户、assistant 与工具历史按常规会话和 compaction(上下文压缩)规则增长。Header、卡片、计划、Markdown 样式和快捷键不增加 token。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
只要组合后的提示词、schema、路由和保留历史前缀保持稳定,就只追加。组合变更与压缩可能从第一个变化的 token 起使复用失效。
|
||||
只要组合后的提示词、schema、路由和保留历史前缀保持稳定,就保持仅追加。组合方式变更与 compaction 可能从第一个变化的 token 起使复用失效。
|
||||
|
||||
### 人类问题答案
|
||||
|
||||
@@ -102,11 +102,11 @@
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
只追加;答案跟在可复用请求前缀之后。
|
||||
仅追加;答案跟在可复用请求前缀之后。
|
||||
|
||||
## 已知限制与延后工作
|
||||
|
||||
- **只支持 TTY**:stdin 与 stdout 都必须是终端;自动化使用 `dsh-cli-demo`。
|
||||
- **一个已配置的终端会话**:记录与编辑器绑定到一个确切会话 id。
|
||||
- **一个已配置的终端会话**:transcript 与编辑器绑定到一个确切会话 id。
|
||||
- **应用集群固定不变**:JSONL 持久化与 ask-user 工具内置;不同策略需要另一种组合。
|
||||
- **批准机制独立存在**:此应用回答 `ctx.userInteraction`,而不是 `ctx.approval`;权限提示需要批准服务和回答方。
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
文件系统栈包括:提供方 seam(文本 I/O 与带可选版本防护的原子变更)、本地实现、政策门禁插件(已观察状态、编辑前读取、版本防护的写入/编辑)、面向模型的文件工具与执行器,以及基于 bash 的发现工具。全部都是**产品** 包。
|
||||
文件系统栈包括:提供方 seam(文本 I/O 与带可选版本防护的原子变更)、本地实现、策略门禁插件(已观察状态、编辑前读取、版本防护的写入/编辑)、面向模型的文件工具与执行器,以及基于 bash 的发现工具。全部都是**产品**包(package)。
|
||||
|
||||
| 包 | 角色 | ctx 键 |
|
||||
|---|---|---|
|
||||
| `fs/` | 提供方 seam:文本 I/O 与原子变更原语(可选版本防护);拥有 `fs/*` 政策事件 | `ctx.fs` |
|
||||
| `fs/` | 提供方 seam:文本 I/O 与原子变更原语(可选版本防护);拥有 `fs/*` 策略事件 | `ctx.fs` |
|
||||
| `fs-local/` | 本地文件系统 `FileSystem` 实现 | (注册 `ctx.fs`) |
|
||||
| `fs-sandbox/` | 强制沙箱的 `FileSystem`:扩展 `fs-local`,并按每次调用的模式与工作区根政策约束写入/编辑(只读模式拒绝,工作区写入模式限制在会话工作区与临时根目录内);读取直接通过 | (注册 `ctx.fs`) |
|
||||
| `fs-policy/` | 政策门禁插件:通过 `fs/*` 事件门禁提供已观察状态、编辑前读取和版本防护的写入/编辑 | (无服务,仅有 `fs/*` 监听器) |
|
||||
| `tool-fs/` | 面向模型的 `read`/`write`/`edit` 工具以及执行器(通过 `ctx.fs` 读取,拥有读取窗口逻辑,分派 `fs/*`);为会话 cwd 相对路径保留文件系统语义,并在已挂载的 `ctx.fs` 实施约束时声明沙箱升级字段 | (注册到 `ctx.tools`) |
|
||||
| `fs-sandbox/` | 强制沙箱的 `FileSystem`:扩展 `fs-local`,并按每次调用的模式与工作区根目录策略约束写入/编辑(只读模式拒绝,工作区写入模式限制在会话工作区与临时根目录内);读取则直接放行 | (注册 `ctx.fs`) |
|
||||
| `fs-policy/` | 策略门禁插件:通过 `fs/*` 事件门禁提供已观察状态、编辑前读取和版本防护的写入/编辑 | (无服务,仅有 `fs/*` 监听器) |
|
||||
| `tool-fs/` | 面向模型的 `read`/`write`/`edit` 工具以及执行器(通过 `ctx.fs` 读取,拥有读取窗口逻辑,分派 `fs/*`);为会话 cwd 相对路径保留文件系统语义,并在已挂载的 `ctx.fs` 实施约束时声明沙箱升权字段 | (注册到 `ctx.tools`) |
|
||||
| `tool-fs-search/` | 面向模型的 `glob`/`grep` 发现工具;当 `rg` 位于 bash 执行器 `PATH` 上时注册,通过 `ctx.bash` 运行固定 ripgrep 命令,而不是使用 `ctx.fs` 提供方方法 | (注册到 `ctx.tools`) |
|
||||
|
||||
接口位于 `fs/fs/`。沙箱化、远程或限定项目作用域的文件系统后端可以替换 `fs-local`,而无需更改 seam、政策门禁或面向模型的工具 schema;`fs-sandbox` 是第一个这样的替代实现(基于共享沙箱模式的进程内路径围栏;见[跨能力族 fs 沙箱 Agent Note](../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md))。政策(`fs-policy/`)是一个只通过 `fs/*` 事件门禁参与的插件,不是工具注入的服务;因此移除它会平稳失去政策,留下不受约束的裸提供方,而不会破坏工具。加载 `tool-fs/` 的部署也应加载该插件。模式围栏与编辑前读取门禁彼此正交,可以组合。发现(`tool-fs-search/`)有意不扩展提供方 seam:搜索是在 bash 执行器上运行 `rg`、由进程支持的工作流,因此文件系统后端无需承担通用搜索契约;只有当执行器能找到 `rg` 时,其工具才会注册。如果 bash 工作目录与 `read` 根目录是同一工作区,结果就能继续读取,这也是其 README 所述的共置部署。
|
||||
接口位于 `fs/fs/`。沙箱化、远程或限定项目作用域的文件系统后端可以替换 `fs-local`,而无需更改 seam、策略门禁或面向模型的工具 schema;`fs-sandbox` 是第一个这样的替代实现(基于共享沙箱模式的进程内路径围栏;见[跨能力族 fs 沙箱 Agent Note(agent 决策记录)](../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md))。策略(`fs-policy/`)是一个只通过 `fs/*` 事件门禁参与的插件,不是工具注入的服务;因此移除它只会使策略失效,留下不受约束的裸提供方,而不会破坏工具。加载 `tool-fs/` 的部署也应加载该插件。模式围栏与编辑前读取门禁彼此正交,可以组合。发现(`tool-fs-search/`)有意不扩展提供方 seam:搜索是在 bash 执行器上运行 `rg`、基于进程的工作流,因此文件系统后端无需承担通用搜索契约;只有当执行器能找到 `rg` 时,其工具才会注册。如果 bash 工作目录与 `read` 根目录是同一工作区,其结果便可供后续读取,这也是其 README 所述的共置部署。
|
||||
|
||||
## 文件 I/O 不设超时
|
||||
|
||||
`read`/`write`/`edit` **不** 接受 `timeoutMs`,提供方 seam 也不启动 deadline。这与 bash 和 web(两者使用 [`@deepseek-ai/dsh-timeout`](../util/timeout/README.md))及基于 bash 的 `glob`/`grep` 不同(它们声明的 `timeoutMs` 由 `@deepseek-ai/dsh-timeout-policy` 强制执行):这些工作由进程支持,deadline 可以实际终止工作。本地系统调用至多只能尽力中止:超时无法强制正在进行的 `fsync`/`rename` 停止,因此这里的 deadline 会成为无法兑现承诺的配置项。在此添加 deadline 还会在「显式优于隐式」明确禁止的地方引入隐式默认值。两个参考 agent(Claude Code、Codex)出于同一原因都不为文件 I/O 计时;取消仍通过工具执行信号传播,在系统调用边界尽力中止。
|
||||
`read`/`write`/`edit` **不** 接受 `timeoutMs`,提供方 seam 也不设置截止期限。这与 bash 和 web(两者使用 [`@deepseek-ai/dsh-timeout`](../util/timeout/README.md))及基于 bash 的 `glob`/`grep` 不同(它们声明的 `timeoutMs` 由 `@deepseek-ai/dsh-timeout-policy` 强制执行):这些工作基于进程运行,截止期限可以实际终止工作。本地系统调用至多只能尽力中止:超时无法强制正在进行的 `fsync`/`rename` 停止,因此这里的截止期限会成为无法兑现承诺的配置项。在此添加截止期限还会在「显式优于隐式」明确禁止的地方引入隐式默认值。两个参考 agent(Claude Code、Codex)出于同一原因都不为文件 I/O 计时;取消仍通过工具执行信号传播,在系统调用边界尽力中止。
|
||||
|
||||
@@ -14,18 +14,18 @@ await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
|
||||
|
||||
## 行为
|
||||
|
||||
- **`resolve(path, opts?)`**:相对 `path` 在调用方提供 `opts.cwd` 时以该值为基准解析(面向模型的工具会传入调用 agent(智能体)的会话 cwd;见[每会话 cwd Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-fs-per-session-cwd.md)),否则以 `config.cwd` 为基准(默认 `process.cwd()`);绝对 `path` 会忽略两者。`opts.signal` 会在本地解析前后检查,远程同级后端则可以用它中止往返。`targetKey` 是文件的 `realpath`,因此经符号链接到达同一文件的两个输入路径会共享一个身份,写入/编辑落在链接目标上,同时保留链接。尚不存在的路径在父目录存在时使用 realpath 后的父目录加 basename;只有父目录无法解析时才回退到绝对路径。`displayPath` 是绝对但未经解析的路径。
|
||||
- **`stat` / `lstat`**:返回目标元数据;目标不存在时返回 `undefined`。`stat` 为已解析目标报告 `FsInfo`(`version` 是由 bigint `dev:ino:size:mtimeNs:ctimeNs` 派生的不透明 token,`type` 为 `file`/`directory`/`other`,`size` 以字节计);路径形态的 `lstat` 不跟随最后一个符号链接,报告 `FsPathInfo`,因此可以返回 `symlink`。两者都会在异步元数据探测前后检查取消,因此飞行中的中止会报告 `FS_ABORTED`,而非陈旧的不存在结果。
|
||||
- **`readText` / `streamText`**:只支持 UTF-8。`readText` 读取整个文件;`streamText` 按分片流式读取(跨分片解码),因此超大文件无需整体保存在内存中。两者都会拒绝无效 UTF-8、包含 NUL 字节的二进制样本(`FS_NOT_TEXT`)以及非普通文件目标。`read` 工具(`@deepseek-ai/dsh-tool-fs`)按大小决定调用哪个方法,并拥有行窗口逻辑。
|
||||
- **`resolve(path, opts?)`**:相对 `path` 在调用方提供 `opts.cwd` 时以该值为基准解析(面向模型的工具会传入调用 agent(智能体)的会话 cwd;见[每会话 cwd Agent Note(agent 决策记录)](../../../.agents/notes/implemented/architecture/2026-07-02-fs-per-session-cwd.md)),否则以 `config.cwd` 为基准(默认 `process.cwd()`);绝对 `path` 会忽略两者。`opts.signal` 会在本地解析前后检查,远程同级后端则可以用它中止往返。`targetKey` 是文件的 `realpath`,因此经符号链接到达同一文件的两个输入路径会共享一个身份,写入/编辑落在链接目标上,同时保留链接。尚不存在的路径在父目录存在时使用 realpath 后的父目录加 basename;只有父目录无法解析时才回退到绝对路径。`displayPath` 是绝对但未经解析的路径。
|
||||
- **`stat` / `lstat`**:返回目标元数据;目标不存在时返回 `undefined`。`stat` 为已解析目标报告 `FsInfo`(`version` 是由 bigint `dev:ino:size:mtimeNs:ctimeNs` 派生的不透明 token,`type` 为 `file`/`directory`/`other`,`size` 以字节计);路径形态的 `lstat` 不跟随最后一个符号链接,报告 `FsPathInfo`,因此可以返回 `symlink`。两者都会在异步元数据探测前后检查取消,因此异步探测进行期间发生的中止会报告 `FS_ABORTED`,而非已失效的“不存在”结果。
|
||||
- **`readText` / `streamText`**:只支持 UTF-8。`readText` 读取整个文件;`streamText` 按分片流式读取(跨分片解码),因此超大文件无需整体保存在内存中。两者都会拒绝无效 UTF-8、包含 NUL 字节的二进制样本(`FS_NOT_TEXT`)以及非普通文件目标。`read` 工具(`@deepseek-ai/dsh-tool-fs`)按大小决定调用哪个方法,并负责行窗口逻辑。
|
||||
- **`listDir`**:按稳定的 `name.localeCompare()` 顺序列出一层目录。每个条目携带子项 basename、类型、解析后的子目标(`displayPath` 位于所列目录下,`targetKey` 是 realpath 身份)和低成本 stat 元数据(`version`,普通文件另有 `size`)。它绝不会打开或解码文件内容。缺失目标报告 `FS_NOT_FOUND`,文件/特殊文件目标报告 `FS_NOT_DIRECTORY`,已中止调用报告 `FS_ABORTED`,权限失败报告 `FS_PERMISSION_DENIED`,其他列出或子项元数据 I/O 失败报告 `FS_IO_ERROR`。损坏/消失的子项以无元数据的 `other` 返回,但解析子项时出现权限/I/O 失败会让整个列表以结构化 `FsError` 失败。
|
||||
- **`writeText`**:原子写入。它会向排他打开的临时文件(`wx`、`0o600`)写入;该文件位于目标旁随机命名的私有暂存目录(`0o700`)内。完成写入和 fsync 后,以 rename 覆盖目标。现有文件的 mode 会保留,新文件默认为 `0o600`;Windows 上的新文件继承目标目录的 DACL,而替换会在写入前把目标 DACL 复制到空临时文件,并通过 `ReplaceFileW` 发布,使原访问政策得以保留(见 [Windows DACL 保留 Agent Note](../../../.agents/notes/implemented/bug-fix/2026-07-19-windows-atomic-write-dacl-preservation.md))。`expected` 防护是可选的:省略时无条件创建或覆盖;`createIfAbsent` 创建缺失目标并拒绝现有目标(`FS_NOT_OBSERVED`);`replaceIfVersion` 只在观察到的版本上替换(目标缺失或版本不匹配均为 `FS_STALE_VERSION`)。
|
||||
- **`editText`**:在同一原语之上依次执行原子的字面量读取、修改和写入,并通过变更锁按目标串行化。`expected` 防护是可选的:提供时,会在字面量匹配之前校验版本(陈旧编辑报告 `FS_STALE_VERSION`,绝不会针对较新内容报告 `FS_EDIT_NOT_FOUND`/`FS_AMBIGUOUS_EDIT`);省略时,无条件编辑当前内容。无论哪种情况,目标缺失都报告 `FS_STALE_VERSION`。匹配时规范化为 LF,随后恢复文件主要的 CRLF/LF 风格;空 `oldString` / 零匹配报告 `FS_EDIT_NOT_FOUND`,未设置 `replace_all` 的多个匹配则报告 `FS_AMBIGUOUS_EDIT`。
|
||||
- **`writeText`**:原子写入。它会向排他打开的临时文件(`wx`、`0o600`)写入;该文件位于目标旁随机命名的私有暂存目录(`0o700`)内。完成写入和 fsync 后,以 rename 覆盖目标。现有文件的 mode 会保留,新文件默认为 `0o600`;Windows 上的新文件继承目标目录的 DACL,而替换会在写入前把目标 DACL 复制到空临时文件,并通过 `ReplaceFileW` 发布,使原访问策略得以保留(见 [Windows DACL 保留 Agent Note](../../../.agents/notes/implemented/bug-fix/2026-07-19-windows-atomic-write-dacl-preservation.md))。`expected` 防护是可选(OPTIONAL)的:省略时无条件创建或覆盖;`createIfAbsent` 创建缺失目标并拒绝现有目标(`FS_NOT_OBSERVED`);`replaceIfVersion` 只在观察到的版本上替换(目标缺失或版本不匹配均为 `FS_STALE_VERSION`)。
|
||||
- **`editText`**:在同一原语之上执行原子式的字面量读取-修改-写入,并通过变更锁按目标串行化。`expected` 防护是可选(OPTIONAL)的:提供时,会在字面量匹配之前校验版本(陈旧编辑报告 `FS_STALE_VERSION`,绝不会针对较新内容报告 `FS_EDIT_NOT_FOUND`/`FS_AMBIGUOUS_EDIT`);省略时,无条件编辑当前内容。无论哪种情况,目标缺失都报告 `FS_STALE_VERSION`。匹配时规范化为 LF,随后恢复文件主要的 CRLF/LF 风格;空 `oldString` / 零匹配报告 `FS_EDIT_NOT_FOUND`,未设置 `replace_all` 的多个匹配则报告 `FS_AMBIGUOUS_EDIT`。
|
||||
|
||||
包根 SDK 接口包含默认/具名 `LocalFileSystem` 类和 `Config`。原始 I/O 位于 `src/fsio.ts`(不依赖 Cordis,单独进行单元测试);`src/index.ts` 是轻量服务接线。
|
||||
包(package)根目录的 SDK 接口包含默认/具名 `LocalFileSystem` 类和 `Config`。原始 I/O 位于 `src/fsio.ts`(不依赖 Cordis,单独进行单元测试);`src/index.ts` 是轻量服务接线。
|
||||
|
||||
## 模型体验
|
||||
|
||||
通过 [`dsh-tool-fs`](../tool-fs/README.md) 间接产生影响;该消费方把本提供方带行窗口的 UTF-8 内容、变更确认和精确提供方消息渲染为有上限且保留的结果,而版本、原子写入机制和目录元数据保持内部可见。
|
||||
通过 [`dsh-tool-fs`](../tool-fs/README.md) 间接产生影响;该消费方在有上限的保留结果中渲染本提供方带行窗口的 UTF-8 内容、变更确认和精确提供方消息,而版本、原子写入机制和目录元数据仍属内部实现。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
**fs-policy 插件**:它在 `ctx.fs` 提供方 seam([`@deepseek-ai/dsh-fs`](../fs))之上增加已观察状态、编辑前读取和版本防护的写入/编辑;它通过 `fs/*` 事件门禁参与,**不是** 通过方法服务。该插件**不** 注册 `ctx.fsPolicy` 服务,也没有公开的 `read`/`write`/`edit`/`resolve` 方法。它是文件系统栈的政策层:不是可替换 seam,而是不应位于 `FileSystem` 提供方基类上的政策。
|
||||
**fs-policy 插件**:它在 `ctx.fs` 提供方 seam([`@deepseek-ai/dsh-fs`](../fs))之上增加已观察状态、编辑前读取和版本防护的写入/编辑;它通过 `fs/*` 事件门禁参与,**不是**通过方法服务。该插件**不**注册 `ctx.fsPolicy` 服务,也没有公开的 `read`/`write`/`edit`/`resolve` 方法。它是文件系统栈的策略层:不是可替换 seam,而是不应位于 `FileSystem` 提供方基类上的策略。
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
@@ -20,10 +20,10 @@ await ctx.plugin(FsPolicy)
|
||||
|
||||
## 四层拆分
|
||||
|
||||
| 层 | 包 | 角色 |
|
||||
| 层 | 包(package) | 角色 |
|
||||
|---|---|---|
|
||||
| 工具/执行器 | `@deepseek-ai/dsh-tool-fs` | 面向模型的 schema、读取窗口和文本渲染;通过 `ctx.fs` 读取/写入/编辑,并分派 `fs/*` 事件 |
|
||||
| 政策 | `@deepseek-ai/dsh-fs-policy`(本包) | 已观察状态、编辑前读取和版本防护的写入/编辑,通过 `fs/*` 事件门禁贡献(无服务) |
|
||||
| 策略 | `@deepseek-ai/dsh-fs-policy`(本包) | 通过 `fs/*` 事件门禁提供已观察状态、编辑前读取和版本防护的写入/编辑(无服务) |
|
||||
| 提供方 seam | `@deepseek-ai/dsh-fs` | `ctx.fs`:文本 I/O 与原子变更原语(可选版本防护);拥有 `fs/*` 事件词汇 |
|
||||
| 提供方 | `@deepseek-ai/dsh-fs-local` | `ctx.fs` 的本地实现 |
|
||||
|
||||
@@ -39,7 +39,7 @@ await ctx.plugin(FsPolicy)
|
||||
|
||||
## 已观察状态是先前观察记录;新鲜度由提供方 CAS 保证
|
||||
|
||||
已观察状态是一张从弱引用所有者映射到目标版本的表,每次读取或变更成功后都会更新;记录存在本身就是先前观察凭据。插件不执行文件系统 I/O:它把观察到的版本提供给提供方的原子变更防护。窗口读取会观察整个文件的版本,因此只有文件保持不变时才允许后续的定向编辑。插件 dispose(资源释放)时会丢弃状态,并且不会跨会话持久化。
|
||||
已观察状态是一张以所有者为弱键、记录各目标版本的映射表,每次读取或变更成功后都会更新;记录存在本身就是先前观察凭据。插件不执行文件系统 I/O:它把观察到的版本提供给提供方的原子变更防护。窗口读取会观察整个文件的版本,因此只有文件保持不变时才允许后续的定向编辑。插件 dispose(资源释放)时会丢弃状态,并且不会跨会话持久化。
|
||||
|
||||
## 单槽、先到者胜
|
||||
|
||||
@@ -47,7 +47,7 @@ await ctx.plugin(FsPolicy)
|
||||
|
||||
## 不与方法耦合
|
||||
|
||||
由于插件只通过事件影响外部世界,移除它不会在服务注入边界破坏 `@deepseek-ai/dsh-tool-fs`:工具会直接落到裸 `ctx.fs` 提供方(无条件写入/编辑,无已观察状态)。重新加载则会再次叠加政策。相比必需的方法服务,这种可平稳增删的性质正是事件门禁的全部目的。
|
||||
由于插件只通过事件影响外部世界,移除它不会在服务注入边界破坏 `@deepseek-ai/dsh-tool-fs`:工具会直接落到裸 `ctx.fs` 提供方(无条件写入/编辑,无已观察状态)。重新加载则会再次叠加策略。相比必需的方法服务,这种可平稳增删的性质正是事件门禁的全部目的。
|
||||
|
||||
## 模型体验
|
||||
|
||||
@@ -67,7 +67,7 @@ await ctx.plugin(FsPolicy)
|
||||
|
||||
## 已知限制与延期工作
|
||||
|
||||
- **已观察状态无法跨会话恢复**:`WeakMap` 记录的持久化延期处理,因此恢复的会话必须重新读取文件,才能执行防护写入/编辑。
|
||||
- **没有 agent 会话的参与者绝无法满足政策**:它们的编辑会抛出 `FS_NOT_OBSERVED`,写入总会解析为 `createIfAbsent`,因此非 agent(智能体)调用方无法通过门禁覆盖现有文件。
|
||||
- **已观察状态无法在会话恢复后保留**:`WeakMap` 记录的持久化工作延期处理,因此恢复的会话必须重新读取文件,才能执行防护写入/编辑。
|
||||
- **没有 agent(智能体)会话的参与者绝无法满足策略**:它们的编辑会抛出 `FS_NOT_OBSERVED`,写入总会解析为 `createIfAbsent`,因此非 agent 调用方无法通过门禁覆盖现有文件。
|
||||
- **直接 `ctx.fs` 读取不会发出 `fs/observed`**:在 `read` 工具之外读取的文件仍未观察;后续防护编辑会以 `FS_NOT_OBSERVED` 拒绝,直到工具读取该文件。
|
||||
- **授权依据是版本新鲜度,而非视图完整性**:任何窗口读取都会授权对未变文件执行全文件覆盖,这有意弱于完整视图规则(见 [seam 拆分 Agent Note](../../../.agents/notes/implemented/simplification/2026-06-26-fsspec-style-fs-seam.md))。
|
||||
- **授权依据是版本新鲜度,而非视图完整性**:任何窗口读取都会授权对未变文件执行全文件覆盖,这有意弱于完整视图规则(见 [seam 拆分 Agent Note(agent 决策记录)](../../../.agents/notes/implemented/simplification/2026-06-26-fsspec-style-fs-seam.md))。
|
||||
|
||||
@@ -4,21 +4,21 @@
|
||||
|
||||
`SandboxedFileSystem` 扩展 [`LocalFileSystem`](../fs-local/README.md) 并注册为 `ctx.fs`。它逐字继承全部文本存储机制(解析、stat、读取/流式读取、列出、原子写入、按读取、匹配、写入顺序执行的编辑临界区),只为 `writeText`/`editText` 增加按调用的模式围栏。读取始终直接通过:所有模式都允许读取。
|
||||
|
||||
只需加载它来替代 `dsh-fs-local`,并同时加载 [`ctx.sandboxPolicy`](../../sandbox/sandbox-policy/README.md),即可完成替换;面向模型的工具(`dsh-tool-fs`)无需改动。工具层把调用会话的模式和 cwd 解析为与 bash 相同的按调用政策,因此两个能力族绝不会约束到不同根目录。
|
||||
只需加载它来替代 `dsh-fs-local`,并同时加载 [`ctx.sandboxPolicy`](../../sandbox/sandbox-policy/README.md),即可完成替换;面向模型的工具(`dsh-tool-fs`)无需改动。工具层把调用会话的模式和 cwd 解析为与 bash 相同的按调用策略,因此两个能力族绝不会约束到不同根目录。
|
||||
|
||||
## 围栏
|
||||
|
||||
按调用政策携带有效模式(会话覆盖值或升级授权)和调用会话不可变的 cwd 根目录;只有没有会话的调用才回退到部署政策:
|
||||
按调用策略携带有效模式(会话覆盖值或升级授权)和调用会话不可变的 cwd 根目录;只有没有会话的调用才回退到部署策略:
|
||||
|
||||
- `read-only`:以结构化 `FS_SANDBOX_DENIED` 拒绝所有变更;
|
||||
- `workspace-write`:只有目标规范化后位于可写根目录下,才允许变更。可写根包括工作区根目录和平台临时区域(`/tmp`、`os.tmpdir()`),与 Seatbelt profile 授权的集合相同;该集合由唯一的 [`writableRoots`](../../sandbox/README.md) 函数派生,使 fs 围栏与 bash runner 不会漂移。规范拼写使用词法快速路径;基于身份的祖先回退可以识别 Windows 长名称和 8.3 名称等别名等价根目录,而不会把无关前缀视为包含关系。委托前会立即重新规范化目标,因此工具解析后被替换的祖先符号链接也会被发现;
|
||||
- `danger-full-access`:不加围栏直接委托。
|
||||
|
||||
## 威胁模型:政策围栏,而非内核边界
|
||||
## 威胁模型:策略围栏,而非内核边界
|
||||
|
||||
围栏是在可信代码中检查模型控制的路径。操作本身属于 seam(open、rename),只有目标路径不可信,因此「规范化后检查包含关系」就是该接口的完整答案。这与 `code-runtime` 的立场相同:提供约束,但不是安全边界。不可信代码的内核级隔离仍由 `ctx.bash` 负责([`dsh-bash-sandbox`](../../bash/bash-sandbox/README.md))。剩余 TOCTOU(在包含关系复查与系统调用之间替换祖先符号链接)会通过写入前立即重新规范化来缩小,并为该威胁模型所接受;内核严密边界需要 `openat2` 一类原语,其可移植性成本在此不值得。
|
||||
|
||||
拒绝是结构化 `FsError`(`FS_SANDBOX_DENIED`,携带有效模式),不通过 stderr 文本推断(不同于 bash 的内核拒绝),因为进程内围栏准确知道自己拒绝了什么。面向模型的 `[sandbox: file access denied under <mode> mode]` 标记以及唯一一次获批的更宽权限重试位于工具层(`dsh-tool-fs`),与 bash 完全相同。见[跨能力族 fs 沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md)。
|
||||
拒绝是结构化 `FsError`(`FS_SANDBOX_DENIED`,携带有效模式),不通过 stderr 文本推断(不同于 bash 的内核拒绝),因为进程内围栏准确知道自己拒绝了什么。面向模型的 `[sandbox: file access denied under <mode> mode]` 标记以及唯一一次获批的更宽权限重试位于工具层(`dsh-tool-fs`),与 bash 完全相同。见[跨能力族 fs 沙箱 Agent Note(agent 决策记录)](../../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md)。
|
||||
|
||||
## 模型体验
|
||||
|
||||
@@ -26,10 +26,10 @@
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
不会直接使缓存失效;具名消费方负责请求前缀的任何变化。
|
||||
不会直接使缓存失效;上述消费方负责请求前缀的任何变化。
|
||||
|
||||
## 已知限制与延期工作
|
||||
|
||||
- **政策围栏,而非内核边界**:该检查是可信代码处理模型控制的路径,因此解析到系统调用之间残留的 TOCTOU 会被原位重新规范化缩小,但不会消除;对抗性宿主进程不在范围内。不可信代码的内核级隔离仍属于 `ctx.bash`。
|
||||
- **策略围栏,而非内核边界**:该检查是可信代码处理模型控制的路径,因此解析到系统调用之间残留的 TOCTOU 会被原位重新规范化缩小,但不会消除;对抗性宿主进程不在范围内。不可信代码的内核级隔离仍属于 `ctx.bash`。
|
||||
- **围栏与 runner 的一致性来自派生,而非断言**:可写集合来自 `writableRoots`,该函数与 Seatbelt profile 共享,并由一致性测试固定;不通过该函数更改可写集合的 runner profile 会发生漂移。
|
||||
- **要求 `ctx.sandboxPolicy`**:工具使用它解析每个会话政策,后端用它处理无 agent 调用的回退;未组合该服务时,后端不会实施约束。
|
||||
- **要求 `ctx.sandboxPolicy`**:工具使用它解析每个会话策略,后端用它处理无 agent 调用的回退;未组合该服务时,后端不会实施约束。
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
**文件系统提供方 seam**:抽象 `FileSystem` 服务(`ctx.fs`),定义后端提供的存储原语,包括路径解析、stat 元数据、不跟随链接的路径元数据、读取/流式读取文本、列出目录、原子写入和应用字面量编辑,但不规定实现方式。两个变更操作都**可选** 接收版本防护,因此 `ctx.fs` 本身就是完整且不受约束的文本存储 seam。本包还拥有由工具分派、政策插件监听的 `fs/*` 政策事件词汇。
|
||||
**文件系统提供方 seam**:抽象 `FileSystem` 服务(`ctx.fs`),定义后端提供的存储原语,包括路径解析、stat 元数据、不跟随链接的路径元数据、读取/流式读取文本、列出目录、原子写入和应用字面量编辑,但不规定实现方式。两个变更操作都**可选**接收版本防护,因此 `ctx.fs` 本身就是完整且不受约束的文本存储 seam。本包(package)还拥有由工具分派、策略插件监听的 `fs/*` 策略事件词汇。
|
||||
|
||||
本包是四层文件系统栈中的提供方 seam 层;该拆分使每个关注点可以独立演进和替换(见[能力 seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)、[文件系统能力 seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.md)、[拆分文件系统 seam Agent Note](../../../.agents/notes/implemented/simplification/2026-06-26-fsspec-style-fs-seam.md)和[文件上下文事件门禁 Agent Note](../../../.agents/notes/implemented/architecture/2026-06-26-file-context-as-event-gate.md)):
|
||||
本包是四层文件系统栈中的提供方 seam 层;该拆分使每个关注点可以独立演进和替换(见[能力 seam Agent Note(agent 决策记录)](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)、[文件系统能力 seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.md)、[拆分文件系统 seam Agent Note](../../../.agents/notes/implemented/simplification/2026-06-26-fsspec-style-fs-seam.md)和[文件上下文事件门禁 Agent Note](../../../.agents/notes/implemented/architecture/2026-06-26-file-context-as-event-gate.md)):
|
||||
|
||||
| 层 | 包 | 角色 |
|
||||
|---|---|---|
|
||||
| 工具/执行器 | `@deepseek-ai/dsh-tool-fs` | 面向模型的 `read`/`write`/`edit` schema、读取窗口和文本渲染;通过 `ctx.fs` 读取/写入/编辑,并分派 `fs/*` 事件 |
|
||||
| 政策 | `@deepseek-ai/dsh-fs-policy` | 已观察状态、编辑前读取和版本防护的写入/编辑,通过 `fs/*` 事件门禁贡献(无服务) |
|
||||
| 策略 | `@deepseek-ai/dsh-fs-policy` | 已观察状态、编辑前读取和版本防护的写入/编辑,通过 `fs/*` 事件门禁贡献(无服务) |
|
||||
| 提供方 seam | `@deepseek-ai/dsh-fs`(本包) | `ctx.fs`:文本 I/O 与原子变更原语(可选版本防护);拥有 `fs/*` 事件词汇 |
|
||||
| 提供方 | `@deepseek-ai/dsh-fs-local` | 宿主文件系统实现 |
|
||||
|
||||
未来的沙箱化、虚拟或远程后端只需实现该接口,政策层和工具层无需改变。
|
||||
未来的沙箱化、虚拟或远程后端只需实现该接口,策略层和工具层无需改变。
|
||||
|
||||
## 服务 API(`ctx.fs`)
|
||||
|
||||
@@ -23,24 +23,24 @@
|
||||
|---|---|
|
||||
| `resolve(path, opts?)` | 把路径解析为稳定的 `FsTarget`(不透明 `targetKey`、`displayPath`)。`opts.cwd` 是相对 `path` 解析所依据的基准(调用方提供其会话工作区;绝对路径忽略该值;省略时使用后端默认值),`opts.signal` 则中止后端往返。该方法是异步的,因为远程后端可能需要 I/O。经不同路径到达的同一文件必须产生相同 `targetKey`。 |
|
||||
| `stat(target, signal?)` | 返回 `FsInfo` 元数据(`version`、`type`、可选 `size`);目标不存在时返回 `undefined`。绝不返回内容。 |
|
||||
| `lstat(path, opts?, signal?)` | 当最后一个路径组件是符号链接时,不跟随该组件,返回 `FsPathInfo` 元数据。该方法采用路径形态,使消费方能在 `resolve` 跟随仓库所有的符号链接进入目标前拒绝它。 |
|
||||
| `lstat(path, opts?, signal?)` | 当最后一个路径组件是符号链接时,不跟随该组件,返回 `FsPathInfo` 元数据。该方法采用路径形态,使消费方能在 `resolve` 跟随仓库自有的符号链接进入目标前拒绝它。 |
|
||||
| `readText(target, signal?)` | 把整个普通文本文件读取为一个解码后的字符串。负责普通文件检查、UTF-8 解码和二进制/NUL 拒绝(`FS_NOT_TEXT`)。 |
|
||||
| `streamText(target, signal?)` | 为大文件按解码后的分片流式读取相同文本(跨分片 UTF-8 解码仍由此处负责)。 |
|
||||
| `listDir(target, signal?)` | 按稳定名称顺序列出直接子项。返回条目名称、条目类型、解析后的子目标和低成本元数据(若可用则包括 `version`/文件 `size`);绝不读取文件内容。缺失目标抛出 `FS_NOT_FOUND`,非目录抛出 `FS_NOT_DIRECTORY`,权限失败抛出 `FS_PERMISSION_DENIED`,其他后端 I/O 失败抛出 `FS_IO_ERROR`。损坏/消失的子项可以作为无元数据的 `other` 返回;子项权限/I/O 失败会使用相同结构化代码使整个列表失败。 |
|
||||
| `listDir(target, signal?)` | 按稳定名称顺序列出直接子项。返回条目名称、条目类型、解析后的子目标和低成本元数据(若可用则包括 `version`/文件 `size`);绝不读取文件内容。缺失目标抛出 `FS_NOT_FOUND`,非目录抛出 `FS_NOT_DIRECTORY`,权限失败抛出 `FS_PERMISSION_DENIED`,其他后端 I/O 失败抛出 `FS_IO_ERROR`。损坏/消失的子项可以作为无元数据的 `other` 返回;子项权限/I/O 失败会使用相同结构化代码使整个列出操作失败。 |
|
||||
| `writeText(target, content, expected?, signal?)` | 原子创建/替换。`expected` 是可选的:省略 ⇒ 无条件创建或覆盖;提供 `FsWriteIntent`(`createIfAbsent`/`replaceIfVersion`)⇒ 添加防护。 |
|
||||
| `editText(target, edit, expected?, signal?)` | 字面量编辑。`expected` 是可选的:省略 ⇒ 无条件编辑当前内容;提供 `{ version }` ⇒ 添加防护,并在匹配之前校验。无论哪种情况,目标缺失都报告 `FS_STALE_VERSION`。应用和写入以原子方式完成,使用同一个变更临界区。 |
|
||||
|
||||
无论是否有版本防护,变更都在后端的每目标锁内运行,因此无条件写入/编辑仍是原子的;「无条件」只移除*版本*前置条件,不移除原子性。
|
||||
|
||||
## `fs/*` 政策事件
|
||||
## `fs/*` 策略事件
|
||||
|
||||
本包声明三个事件(见已生成的[事件目录](../../../docs/cordis-catalog/events.md)),使发出方(`@deepseek-ai/dsh-tool-fs`)和政策监听器(`@deepseek-ai/dsh-fs-policy`)共享词汇,而无需让发出方依赖政策插件。`fs/write-intent` 和 `fs/edit-intent` 是单槽决策 waterfall(监听器完整决策,绝不调用 `next()`);`fs/observed` 是发后即忘的记录事件。它们只携带 `dsh-fs` 词汇和一个不透明 `object` 参与者,不含面向模型的概念或 agent(智能体)/会话所有者结构。
|
||||
本包声明三个事件(见已生成的[事件目录](../../../docs/cordis-catalog/events.md)),使发出方(`@deepseek-ai/dsh-tool-fs`)和策略监听器(`@deepseek-ai/dsh-fs-policy`)共享词汇,而无需让发出方依赖策略插件。`fs/write-intent` 和 `fs/edit-intent` 是单槽决策 waterfall(瀑布式事件)(监听器完整决策,绝不调用 `next()`);`fs/observed` 是发后即忘的记录事件。它们只携带 `dsh-fs` 词汇和一个不透明 `object` 参与者,不含面向模型的概念或 agent(智能体)/会话所有者结构。
|
||||
|
||||
## 提供方 seam,不是政策层
|
||||
## 提供方 seam,不是策略层
|
||||
|
||||
`ctx.fs` 有意接近 fsspec 风格的存储原语,比字节级 `cat`/`open` 高半层,因为它会解码文本并拒绝二进制,使政策层绝不接触原始字节。它负责 UTF-8 解码、二进制拒绝、原子写入和字面量编辑临界区。它**不** 负责行窗口、编号行、渲染 footer 或已观察状态。已观察状态、编辑前读取和版本防护的写入/编辑属于插件(`@deepseek-ai/dsh-fs-policy`)通过提供可选防护而添加的政策,并非提供方行为,因此沙箱化/远程后端不会继承任何面向模型的观察政策。
|
||||
`ctx.fs` 有意接近 fsspec 风格的存储原语,比字节级 `cat`/`open` 高半层,因为它会解码文本并拒绝二进制,使策略层绝不接触原始字节。它负责 UTF-8 解码、二进制拒绝、原子写入和字面量编辑临界区。它**不**负责行窗口、编号行、渲染 footer 或已观察状态。已观察状态、编辑前读取和版本防护的写入/编辑属于插件(`@deepseek-ai/dsh-fs-policy`)通过提供可选防护而添加的策略,并非提供方行为,因此沙箱化/远程后端不会继承任何面向模型的观察策略。
|
||||
|
||||
`editText` 留在该 seam 上,不由政策层通过读取加写入组合,因为版本防护、字面量匹配和原子重写必须处于同一临界区内,才能正确归因错误并实现一方胜出/一方陈旧的并发;远程后端也可以将其实现为原生比较并编辑操作。
|
||||
`editText` 留在该 seam 上,不由策略层通过读取加写入组合,因为版本防护、字面量匹配和原子重写必须处于同一临界区内,才能正确归因错误并实现一方胜出/一方陈旧的并发;远程后端也可以将其实现为原生比较并编辑操作。
|
||||
|
||||
## 词汇
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
不会直接使缓存失效;具名消费方负责请求前缀的任何变化。
|
||||
不会直接使缓存失效;上述消费方负责请求前缀的任何变化。
|
||||
|
||||
## 已知限制与延期工作
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
**面向模型的文件系统发现工具**(`glob`、`grep`)由 **bash 执行器 seam** 支持,而不是由 `ctx.fs` 提供方方法支持。加载时,本包探测 `command -v rg`,探测通过 `ctx.bash` 进行;如果执行器无法在其 `PATH` 上找到 ripgrep,就记录警告,并且不注册工具或提示词段。每次调用都会组装固定的 ripgrep 命令(所有模型控制的值都经过同一个包私有 shell 引用辅助函数),通过 `ctx.bash.resolve(request)` → `ctx.bash.run(spec)` 作为普通前台工具调用运行,解析原始 `rg` 输出,并返回相对于工作目录的规范值。本包注入 `tools`、`systemPrompt` 和 `bash`,有意**不** 注入 `fs`;格式化结果 spill 为可选功能,因此机会性读取 `ctx.spillStore`,调用方式为 `ctx.get()`。
|
||||
**面向模型的文件系统发现工具**(`glob`、`grep`)由 **bash 执行器 seam** 支持,而不是由 `ctx.fs` 提供方方法支持。加载时,本包(package)探测 `command -v rg`,探测通过 `ctx.bash` 进行;如果执行器无法在其 `PATH` 上找到 ripgrep,就记录警告,并且不注册工具或提示词段。每次调用都会组装固定的 ripgrep 命令(所有模型控制的值都经过同一个包私有 shell 引用辅助函数),通过 `ctx.bash.resolve(request)` → `ctx.bash.run(spec)` 作为普通前台工具调用运行,解析原始 `rg` 输出,并返回相对于工作目录的规范值。本包注入 `tools`、`systemPrompt` 和 `bash`,有意**不**注入 `fs`;格式化结果 spill 为可选功能,因此机会性读取 `ctx.spillStore`,调用方式为 `ctx.get()`。
|
||||
|
||||
```ts ignore-check
|
||||
// Default deployment: a bash executor whose PATH includes rg, then the discovery tools.
|
||||
@@ -16,7 +16,7 @@ await ctx.plugin(LocalSpillStore) // @deepseek-ai/dsh-
|
||||
|
||||
## 部署要求:rg 与共置的 bash/文件系统
|
||||
|
||||
已挂载的 bash 执行器必须能在插件加载时解析 `rg`,其来源是执行器的 `PATH`;否则面向模型的工具 schema 中不会出现 `glob` 和 `grep`。返回路径会相对于解析后的 bash 工作目录显示(调用 agent(智能体)存在会话 cwd 时使用该值,否则使用执行器配置的默认值);只有 bash 工作目录与文件系统根目录是同一工作区时,才能用 `read` 继续读取。v1 只记录这项共置要求,不执行运行时跨服务校验;远程或虚拟文件系统搜索需等待共享工作区契约或特定提供方的搜索后端。
|
||||
已挂载的 bash 执行器必须能在插件加载时解析 `rg`,其来源是执行器的 `PATH`;否则面向模型的工具 schema 中不会出现 `glob` 和 `grep`。返回路径会相对于解析后的 bash 工作目录显示(调用方 agent(智能体)有会话 cwd 时使用该 cwd,否则使用执行器配置的默认值);只有 bash 工作目录与文件系统根目录是同一工作区时,才能用 `read` 继续读取。v1 只记录这项共置要求,不执行运行时跨服务校验;远程或虚拟文件系统搜索需等待共享工作区契约或特定提供方的搜索后端。
|
||||
|
||||
## 配置
|
||||
|
||||
@@ -34,14 +34,14 @@ await ctx.plugin(LocalSpillStore) // @deepseek-ai/dsh-
|
||||
|
||||
| 工具 | 参数 | 行为 |
|
||||
|---|---|---|
|
||||
| `glob` | `pattern`、`path?` | 运行 `rg --files --glob <pattern> --sort=modified --no-ignore --hidden`,并排除 VCS 元数据(`.git`、`.svn`、`.hg`、`.bzr`、`.jj`、`.sl`)。`path` 是可选的**目录** 搜索根;省略时使用解析后的 bash 工作目录。每行返回一个路径,按修改时间排序。 |
|
||||
| `grep` | `pattern`、`path?`、`include?` | 按行解析 `rg --json`,避免按冒号拆分的歧义。`pattern` 是 ripgrep 正则表达式;`path` 是可选的**文件或目录** 目标;`include` 是一个正向 glob 过滤器,前置拒绝逗号分隔列表或否定值(`!…`),但允许 `*.{ts,tsx}` 等花括号交替。返回按文件分组、形如 `Line N: <preview>` 的匹配。 |
|
||||
| `glob` | `pattern`、`path?` | 运行 `rg --files --glob <pattern> --sort=modified --no-ignore --hidden`,并排除 VCS 元数据(`.git`、`.svn`、`.hg`、`.bzr`、`.jj`、`.sl`)。`path` 是可选的**目录**搜索根;省略时使用解析后的 bash 工作目录。每行返回一个路径,按修改时间排序。 |
|
||||
| `grep` | `pattern`、`path?`、`include?` | 按行解析 `rg --json`,避免按冒号拆分的歧义。`pattern` 是 ripgrep 正则表达式;`path` 是可选的**文件或目录**目标;`include` 是一个正向 glob 过滤器,前置拒绝逗号分隔列表或否定值(`!…`),但允许 `*.{ts,tsx}` 等花括号交替。返回按文件分组、形如 `Line N: <preview>` 的匹配。 |
|
||||
|
||||
常规预算不进入面向模型的 schema(没有 `head_limit`/`offset`/`case_insensitive`/输出模式):模型需要周边上下文时,用 `read` 读取匹配文件;需要后续结果时,遵循返回的 spill locator 检索提示。
|
||||
|
||||
## 两类预算、两类产物
|
||||
|
||||
原始 `rg` stdout 是内部传输细节。每次搜索从 bash seam 请求 `stdoutMaxBytes: rawOutputMaxBytes`,且只解析完整保留的 stdout;如果执行器仍返回 `stdout.truncated`,搜索会以 `SEARCH_RAW_OUTPUT_OVERFLOW` 失败,并要求模型缩小查询。成功的 `glob` 在 `{ paths }` 中保留所有已取得路径;`grep` 保留所有已取得的 `{ path, lineNumber, line }`,并将其存入 `{ matches }`。内联条目和每行预览上限只应用于 Native 渲染器。直接接口调用的逻辑结果超过内联上限时,后置政策会尽力通过 `ctx.spillStore.saveText()` 保存完整格式化预览,并只把呈现替换为头部页面加 locator。嵌套 Code 分派会跳过 spill,因为其完整规范值不会进入模型上下文。spill 缺失/失败时保留内联页面,并报告完整结果无法保存,绝不会成为 `isError`。
|
||||
原始 `rg` stdout 是内部传输细节。每次搜索从 bash seam 请求 `stdoutMaxBytes: rawOutputMaxBytes`,且只解析完整保留的 stdout;如果执行器仍返回 `stdout.truncated`,搜索会以 `SEARCH_RAW_OUTPUT_OVERFLOW` 失败,并要求模型缩小查询。成功的 `glob` 在 `{ paths }` 中保留所有已取得路径;`grep` 保留所有已取得的 `{ path, lineNumber, line }`,并将其存入 `{ matches }`。内联条目和每行预览上限只应用于 原生渲染器。直接接口调用的逻辑结果超过内联上限时,后置策略会尽力通过 `ctx.spillStore.saveText()` 保存完整格式化预览,并只把呈现替换为头部页面加 locator。嵌套 Code 分派会跳过 spill,因为其完整规范值不会进入模型上下文。spill 缺失/失败时保留内联页面,并报告完整结果无法保存,绝不会成为 `isError`。
|
||||
|
||||
## 错误
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
**面向模型的文件系统工具**(`read`、`write`、`edit`)及其**执行器**。这是文件系统栈的消费方层:拥有工具名称、JSON schema、参数校验、提示词段、**读取窗口逻辑** 和结果格式化。它**直接** 通过 `ctx.fs` 提供方 seam([`@deepseek-ai/dsh-fs`](../fs))读取/写入/编辑:注入 `fs`(以及 `tools`/`systemPrompt`),**不** 注入政策服务。新鲜度/观察政策由独立插件([`@deepseek-ai/dsh-fs-policy`](../fs-policy))通过 `fs/*` 事件门禁贡献;工具不与其方法耦合。
|
||||
**面向模型的文件系统工具**(`read`、`write`、`edit`)及其**执行器**。这是文件系统栈的消费方层:拥有工具名称、JSON Schema、参数校验、提示词段、**读取窗口逻辑**和结果格式化。它**直接**通过 `ctx.fs` 提供方 seam([`@deepseek-ai/dsh-fs`](../fs))读取/写入/编辑:注入 `fs`(以及 `tools`/`systemPrompt`),**不**注入策略服务。新鲜度/观察策略由独立插件([`@deepseek-ai/dsh-fs-policy`](../fs-policy))通过 `fs/*` 事件门禁贡献;工具不与其方法耦合。
|
||||
|
||||
```ts ignore-check
|
||||
// Default deployment: a ctx.fs provider, the policy plugin, then the tools.
|
||||
@@ -11,7 +11,7 @@ await ctx.plugin(FsPolicy) // @deepseek-ai/dsh-fs-po
|
||||
await ctx.plugin(ToolFs) // this package — registers read/write/edit
|
||||
```
|
||||
|
||||
`@deepseek-ai/dsh-fs-policy` 是**可选的**:省略时,工具直接使用裸提供方(无条件写入/覆盖/编辑,无已观察状态)。加载这些工具的部署也应加载该插件,从而提供编辑前读取行为。
|
||||
`@deepseek-ai/dsh-fs-policy` 是**可选的**:省略时,工具直接使用裸提供方(无条件写入/覆盖/编辑,无已观察状态)。加载这些工具的部署也应加载该插件,从而提供写入/编辑前读取行为。
|
||||
|
||||
## 配置
|
||||
|
||||
@@ -24,21 +24,21 @@ await ctx.plugin(ToolFs) // this package — re
|
||||
| `readMaxBytes` | `51200` | 一次 `read` 调用所选行的字节上限;溢出时以「已达上限」footer 结束窗口。 |
|
||||
| `readStreamMinSize` | `10485760` | 大于等于该大小或大小未知的文件采用流式读取,而不是整体加载到内存。 |
|
||||
|
||||
## 工具(schema 见[文件系统工具 schema Agent Note](../../../.agents/notes/implemented/feature/2026-06-17-filesystem-tool-schemas.md))
|
||||
## 工具(schema 见[文件系统工具 schema Agent Note(agent 决策记录)](../../../.agents/notes/implemented/feature/2026-06-17-filesystem-tool-schemas.md))
|
||||
|
||||
| 工具 | 参数 | 行为 |
|
||||
|---|---|---|
|
||||
| `read` | `file_path`、`offset?`、`limit?` | 带行号的 UTF-8 内容和分页 footer。`offset` 从 1 开始;`limit` 默认为配置的 `readLimit`(2000),上限也为该值。 |
|
||||
| `write` | `file_path`、`content` | 创建文件或完整替换文件。有政策插件时:覆盖现有文件要求先在未变版本上执行 `read`;创建新文件不需要。没有插件时:无条件执行。 |
|
||||
| `edit` | `file_path`、非空 `old_string`、`new_string`、`replace_all?` | 字面量替换;除非 `replace_all` 为 true,否则要求唯一匹配。有政策插件时:要求先执行 `read`(任何窗口),且文件此后未变。没有插件时:无条件执行。 |
|
||||
| `write` | `file_path`、`content` | 创建文件或完整替换文件。有策略插件时:覆盖现有文件要求先在未变版本上执行 `read`;创建新文件不需要。没有插件时:无条件执行。 |
|
||||
| `edit` | `file_path`、非空 `old_string`、`new_string`、`replace_all?` | 字面量替换;除非 `replace_all` 为 true,否则要求唯一匹配。有策略插件时:要求先执行 `read`(任何窗口),且文件此后未变。没有插件时:无条件执行。 |
|
||||
|
||||
字段名使用 snake_case,与 Claude Code 和现有 harness 工具 schema 一致。
|
||||
|
||||
规范成功值分别为:`read` → `{ path, offset, lines: [{ number, text }], totalLines }`,`write` → `{ path, operation: 'create' | 'update', before: string | null, after }`,`edit` → `{ path, before, after }`。Native 渲染器会保留下方带行号的读取结果和变更确认。写入/编辑从这些值派生可回放的 diff 卡片元数据;值本身仅用于执行,不会添加到 `tool/result`。
|
||||
规范成功值分别为:`read` → `{ path, offset, lines: [{ number, text }], totalLines }`,`write` → `{ path, operation: 'create' | 'update', before: string | null, after }`,`edit` → `{ path, before, after }`。原生渲染器会保留下方带行号的读取结果和变更确认。写入/编辑从这些值派生可回放的 diff 卡片元数据;这些值本身仅限于本次执行,不会添加到 `tool/result`。
|
||||
|
||||
## 工具就是执行器;政策是事件门禁
|
||||
## 工具就是执行器;策略是事件门禁
|
||||
|
||||
工具**不** 注入政策服务,也不检查任何缓存。每个工具通过 `ctx.fs.resolve(path, { cwd, signal })` 解析路径;它会传入调用 agent(智能体)的会话 cwd(`exec.agent.session.header.cwd`),使相对路径以会话工作区为基准解析并与 `dsh-tool-bash` 一致,同时把工具取消转发到解析过程(见[每会话 cwd Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-fs-per-session-cwd.md))。随后执行:
|
||||
工具**不**注入策略服务,也不检查任何缓存。每个工具通过 `ctx.fs.resolve(path, { cwd, signal })` 解析路径;它会传入调用 agent 的会话 cwd(`exec.agent.session.header.cwd`),使相对路径以会话工作区为基准解析并与 `dsh-tool-bash` 一致,同时把工具取消转发到解析过程(见[每会话 cwd Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-fs-per-session-cwd.md))。随后执行:
|
||||
|
||||
- **read**:一次 `ctx.fs.stat`(用于类型、大小路由和版本),随后调用 `readText`/`streamText`,构建行窗口,再发出 `fs/observed`,使用普通 `ctx.emit`。(1 次 stat。)
|
||||
- **write**:调用 `ctx.waterfall('fs/write-intent', target, exec, () => undefined)` 取得可选防护,然后调用 `ctx.fs.writeText(target, content, intent)`,再发出 `fs/observed`。(0 次 stat。)
|
||||
@@ -52,7 +52,7 @@ await ctx.plugin(ToolFs) // this package — re
|
||||
|
||||
`read` 允许并发调度,因为其唯一变更是同步版本记录器。稍后的 `write` 或 `edit` 会在目标锁内重新检查版本,因此记录器竞态会以拒绝方式关闭;两个变更工具仍保持互斥。见[并行工具调用 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md)。
|
||||
|
||||
包根目录只导出 Cordis 插件契约(`name`、`inject`、`Config` 和 `apply`)。读取渲染(行窗口与输出格式化)位于 `src/read-render.ts`(不依赖 Cordis,单独进行单元测试);`src/read.ts`/`write.ts`/`edit.ts` 是工具执行器,`src/index.ts` 负责组合。
|
||||
包(package)根目录只导出 Cordis 插件契约(`name`、`inject`、`Config` 和 `apply`)。读取渲染(行窗口与输出格式化)位于 `src/read-render.ts`(不依赖 Cordis,单独进行单元测试);`src/read.ts`/`write.ts`/`edit.ts` 是工具执行器,`src/index.ts` 负责组合。
|
||||
|
||||
## 模型体验
|
||||
|
||||
@@ -134,7 +134,7 @@ Use the edit tool for targeted changes to existing UTF-8 text files. It replaces
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
失败会规范化为 `Error: <message>`。本包稳定的校验和读取消息是 `file_path must be a non-empty string`、`limit must be less than or equal to <max>`、`old_string must be a non-empty string`、`old_string and new_string must differ`、`cannot read "<path>": not found`、`cannot read "<path>": not a regular file` 和 `offset <offset> is out of range for "<path>" (<total> lines)`;提供方和政策模板在各自包的 README 中逐字列出。
|
||||
失败会规范化为 `Error: <message>`。本包稳定的校验和读取消息是 `file_path must be a non-empty string`、`limit must be less than or equal to <max>`、`old_string must be a non-empty string`、`old_string and new_string must differ`、`cannot read "<path>": not found`、`cannot read "<path>": not a regular file` 和 `offset <offset> is out of range for "<path>" (<total> lines)`;提供方和策略模板在各自包的 README 中逐字列出。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
@@ -146,6 +146,6 @@ Use the edit tool for targeted changes to existing UTF-8 text files. It replaces
|
||||
|
||||
## 已知限制与延期工作
|
||||
|
||||
- **未交付面向模型的目录列出工具**:`ctx.fs.listDir` 服务于 skill(技能)发现等提供方代码,同级 [`dsh-tool-fs-search`](../tool-fs-search/) 包则提供基于 bash 的 `glob` 与 `grep`,而不是扩展文件系统 seam。
|
||||
- **未交付面向模型的目录列表工具**:`ctx.fs.listDir` 服务于 skill(技能)发现等提供方代码,同级 [`dsh-tool-fs-search`](../tool-fs-search/) 包则提供基于 bash 的 `glob` 与 `grep`,而不是扩展文件系统 seam。
|
||||
- **`read` 只处理 UTF-8 文本文件**:二进制安全读取和 PDF/图像/多模态内容均延期处理;目录目标为 `FS_NOT_REGULAR_FILE`。
|
||||
- **没有超时接口**:`read`/`write`/`edit` 不接受超时参数,也不声明 `timeout-policy` 预算;取消只通过 `exec.signal` 传递(见有意采用的 [fs 能力族立场](../README.md))。
|
||||
|
||||
@@ -7,8 +7,8 @@ goal 家族负责持久目标状态,与消费该状态的面向模型工具和
|
||||
| 包 | 职责 | ctx 键 |
|
||||
|---|---|---|
|
||||
| `goal/` | 事件溯源的目标生命周期、回放折叠、比较并设置变更,以及进程本地激活 | `ctx.goals` |
|
||||
| `goal-session/` | 同会话 goal round 的准入、结果映射与生命周期竞态隔离 | 无 |
|
||||
| `goal-session/` | 同会话 Goal Round 的准入、结果映射与生命周期竞态隔离 | 无 |
|
||||
| `tool-goal/` | 面向模型的读取/创建/更新工具,并在执行时检查权限 | 无 |
|
||||
| `command-goal/` | 面向用户的 `/goal` 状态,以及通过命令平面执行的生命周期控制 | 无 |
|
||||
|
||||
目标状态属于所属会话日志。消费方依赖 `dsh-goal`,而不是具体的 agent loop;续行行为由基于公开 agent seam 的独立插件负责。
|
||||
目标状态是其所属会话日志的一部分。消费方依赖 `dsh-goal`,而不是具体的 agent loop(智能体循环);续行行为由基于公开 agent seam 的独立插件负责。
|
||||
|
||||
@@ -2,22 +2,22 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
面向用户的 `/goal` 控制,基于 [`ctx.goals`](../goal/README.md) 实现。该插件通过 [`ctx.commands`](../../ui/commands/README.md) 注册一个全局命令,因此每个已组合的命令适配器都能发现它;随附 TUI 无需模型轮次即可执行。[用户 goal 命令 Agent Note](../../../.agents/notes/implemented/feature/2026-07-19-human-goal-command.md)负责用户体验与组合决策。
|
||||
面向用户的 `/goal` 控制,基于 [`ctx.goals`](../goal/README.md) 实现。该插件通过 [`ctx.commands`](../../ui/commands/README.md) 注册一个全局命令,因此每个已组合的命令适配器都能发现它;随附 TUI 无需模型轮次即可执行。[用户 goal 命令 Agent Note(agent 决策记录)](../../../.agents/notes/implemented/feature/2026-07-19-human-goal-command.md) 负责用户体验与组合决策。
|
||||
|
||||
## 命令契约
|
||||
|
||||
| 输入 | 结果 |
|
||||
|---|---|
|
||||
| `/goal` | 显示当前目标、持久 phase、round 计数/上限、进程本地激活状态与有效的下一步命令;被阻塞的 goal 还会显示策略代码和说明,没有 goal 时则显示用法。 |
|
||||
| `/goal <objective>` | 创建并激活 goal,或用全新身份替换已完成 goal。未完成 goal 绝不会在没有显式 clear 的情况下被替换。 |
|
||||
| `/goal edit <objective>` | 编辑当前目标,不改变其 phase 或激活状态。编辑已完成 goal 会创建新的 active goal。 |
|
||||
| `/goal pause` | 暂停 active goal,并撤销续行激活。 |
|
||||
| `/goal resume` | 恢复已停止 goal,或在会话 resume/fork 后重新激活 active goal;仍受剩余 round 上限约束。 |
|
||||
| `/goal` | 显示当前目标、持久 phase、Round 计数/上限、进程本地续行启用状态与有效的下一步命令;被阻塞的 goal 还会显示策略代码和说明,没有 goal 时则显示用法。 |
|
||||
| `/goal <objective>` | 创建 goal 并启用续行,或用全新身份替换已完成 goal。未完成 goal 绝不会在没有显式 clear 的情况下被替换。 |
|
||||
| `/goal edit <objective>` | 编辑当前目标,不改变其 phase 或续行启用状态。编辑已完成 goal 会创建新的 active goal。 |
|
||||
| `/goal pause` | 暂停 active goal,并停用续行。 |
|
||||
| `/goal resume` | 恢复已停止 goal,或在会话 resume/fork 后为 active goal 重新启用续行;仍受剩余 Round 上限约束。 |
|
||||
| `/goal clear` | 清除当前指针,同时保留其持久历史和 tombstone。 |
|
||||
|
||||
只有控制词占据完整输入时才不区分大小写。其他任何非空后缀都属于目标,因此 `/goal pause after verification` 会创建该字面目标。goal 领域会修剪并验证目标。由于通用命令平面没有模态编辑器或确认原语,`edit` 会内联接收替换内容;若替换内容不完整,则直接返回错误,提示用户执行 edit 或 clear。
|
||||
只有控制词占据完整输入时才不区分大小写。其他任何非空后缀都属于目标,因此 `/goal pause after verification` 会创建该字面目标。goal 领域会去除目标首尾空白并进行验证。由于通用命令平面没有模态编辑器或确认原语,`edit` 会内联接收替换内容;若试图替换未完成的 goal,则直接返回错误,提示用户执行 edit 或 clear。
|
||||
|
||||
可预期的领域拒绝会变成稳定的直接命令错误,不公开品牌化 id 或 revision。意外实现失败仍会 reject 分发,使适配器能将其报告为命令失败。通用命令文本和输出只属于活跃 UI 状态;每项已接受变更都由 `dsh-goal` 持久化并提供给模型,而不是由此插件完成。
|
||||
可预期的领域拒绝会变成稳定的直接命令错误,不公开带品牌类型的 id 或 revision。意外实现失败仍会 reject 分发,使适配器能将其报告为命令失败。通用命令文本和输出仍属于实时 UI 状态;每项已接受变更都由 `dsh-goal` 持久化并提供给模型,而不是由此插件完成。
|
||||
|
||||
## 组合
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
name: '@deepseek-ai/dsh-command-goal'
|
||||
```
|
||||
|
||||
TUI 应用默认启用完整的持久 goal 栈和此命令。ACP 自动化应用会启用领域与模型工具,但不挂载命令注册表;`goals: false` 会移除该栈。无 UI 的 `agent-spine-demo` 必须显式配置 `goals: {}`,避免无头单次调用方在不知情时从一个物理轮次变为多 round 操作。
|
||||
TUI 应用默认启用完整的持久 goal 栈和此命令。ACP(Agent Client Protocol)自动化应用会启用领域与模型工具,但不挂载命令注册表;`goals: false` 会移除该栈。无 UI 的 `agent-spine-demo` 必须显式配置 `goals: {}`,避免无头单次调用方在不知情时从一个物理轮次变为包含多个 Round 的操作。
|
||||
|
||||
## 模型体验
|
||||
|
||||
@@ -44,7 +44,7 @@ TUI 应用默认启用完整的持久 goal 栈和此命令。ACP 自动化应用
|
||||
|
||||
#### Token 影响
|
||||
|
||||
读取状态或收到直接命令错误不会增加模型 token。每项已接受变更都会增加 goal 领域保留的完整快照;已启用的同会话驱动器还可能增加后续 goal-round 提示词。
|
||||
读取状态或收到直接命令错误不会增加模型 token。每项已接受变更都会增加 goal 领域保留的完整快照;已启用的同会话驱动器还可能增加后续 Goal Round 提示词。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
@@ -52,7 +52,7 @@ TUI 应用默认启用完整的持久 goal 栈和此命令。ACP 自动化应用
|
||||
|
||||
## 已知限制与暂缓工作
|
||||
|
||||
- **仅纯文本交互**:通用命令注册表没有模态编辑表单或替换确认回调;内联 edit 与显式 clear 能在不同适配器中保持确定的破坏性意图。
|
||||
- **没有逐命令 round 上限参数**:`defaultMaxGoalRounds` 仍是部署配置;用户直接请求时,可以要求模型通过另行授权的 goal 工具编辑 `max_goal_rounds`。
|
||||
- **没有持续状态组件**:裸 `/goal` 是可移植的观察接口;适配器专用徽标和可重新连接的命令输出仍属于未来 UI 工作。
|
||||
- **随附应用中只有 TUI 使用此命令**:无头 CLI、ACP 自动化和 JSON-RPC 适配器不消费 `ctx.commands`。如果组合中包含面向模型的 goal 工具,普通提示词仍能授权它们。
|
||||
- **仅纯文本交互**:通用命令注册表没有模态编辑表单或替换确认回调;内联 edit 与显式 clear 能在不同适配器中保持明确且一致的破坏性意图。
|
||||
- **没有逐命令 Round 上限参数**:`defaultMaxGoalRounds` 仍是部署配置;用户直接请求时,可以要求模型通过另行授权的 goal 工具编辑 `max_goal_rounds`。
|
||||
- **没有持续状态组件**:裸 `/goal` 是可移植的观察接口;适配器专用徽标和重连后可恢复的命令输出仍属于未来 UI 工作。
|
||||
- **随附应用中只有 TUI 使用此命令**:无头 CLI(命令行界面)、ACP 自动化和 JSON-RPC 适配器不消费 `ctx.commands`。如果组合中包含面向模型的 goal 工具,普通提示词仍能授权它们。
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
[`ctx.goals`](../goal/README.md) 的同会话续行驱动器。它通过公开 `Agent` 与会话 seam,把活跃且已激活的目标转换为连续的 [goal round](../../../docs/glossary.md#goal-round);[同会话驱动器 Agent Note](../../../.agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.md)负责竞态和生命周期理由。
|
||||
[`ctx.goals`](../goal/README.md) 的同会话续行驱动器。它通过公开 `Agent` 与会话 seam,把 phase 为 active 且已启用续行的目标转换为连续的 [Goal Round](../../../docs/glossary.md#goal-round);[同会话驱动器 Agent Note(agent 决策记录)](../../../.agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.md) 记载竞态与生命周期方面的设计理由。
|
||||
|
||||
## 组合
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
|
||||
## Round 契约
|
||||
|
||||
当完全相同的活跃 agent 处于 idle 状态,且目标 active、已经激活并有剩余容量时,驱动器先为待处理 goal 变更创建检查点,再预留 `roundsStarted + 1`,对应当前 `{ goalId, revision }`。它会排入一条 `<goal_round>` 提示词,并携带 `GoalMessageSource`。通过 `agent/prompt-submit` 准入时,会在下游提示词 hook 前后同时验证完整的排队记录与当前 goal;只有被接受的 `user/message` 才会增加 `roundsStarted`。因陈旧而被拒绝的预留不会消耗 round 编号。
|
||||
当对应的活跃 agent 实例处于 idle 状态,且目标 phase 为 active、已启用续行并有剩余容量时,驱动器先为待处理 goal 变更创建检查点,再预留 `roundsStarted + 1`,对应当前 `{ goalId, revision }`。它会排入一条 `<goal_round>` 提示词,并携带 `GoalMessageSource`。通过 `agent/prompt-submit` 准入时,会在下游提示词钩子前后验证完整的排队记录与当前 goal;只有被接受的 `user/message` 才会增加 `roundsStarted`。因陈旧而被拒绝的预留不会消耗 Round 编号。
|
||||
|
||||
一个 goal round 拥有一个普通会话轮次,该轮次可以包含多个模型/工具步骤。驱动器只会把预留与 `message` 轮次配对,且该轮次必须携带完全相同的 `GoalMessageSource`;可通过声明合并扩展的插件轮次触发器不会准入或替换该预留。用户消息仍是普通轮次,不消耗 goal 上限。如果用户工作在预留前进入 inbox,或加入预留的待处理批次,自动工作会让行,直到用户工作结算;混合批次中的待处理自动提示词会被拒绝,只有 agent 再次 idle 后才重新预留。
|
||||
一个 Goal Round 对应一个普通会话轮次,该轮次可以包含多个模型/工具步骤。驱动器只会把预留与 `message` 轮次配对,且该轮次必须携带完全相同的 `GoalMessageSource`;可通过声明合并扩展的插件轮次触发器不会准入或替换该预留。用户消息仍是普通轮次,不消耗 goal 上限。如果用户工作在预留前进入 inbox,或加入预留的待处理批次,自动工作会让行,直到用户工作结算;混合批次中的待处理自动提示词会被拒绝,只有 agent 再次 idle 后才重新预留。
|
||||
|
||||
保留的提示词会点明经过 JSON 引用的目标与 `round/maxGoalRounds`,将当前工作区、工具结果和持久会话状态视为权威信息,要求在完成前提供证据,并要求在工作仍未完成时保持目标 active。引用可将多行或形似标签的目标文本保留为数据。goal 生命周期变更仍必须通过 `dsh-tool-goal` 的独立权限检查。
|
||||
|
||||
@@ -31,43 +31,43 @@
|
||||
|
||||
| 持久轮次结果 | Goal 操作 | 自动重试 |
|
||||
|---|---|---|
|
||||
| goal 仍 active 且已激活时的 `completed` | 准入下一 round;达到上限时以代码 `round-limit` 阻塞 | 是 |
|
||||
| 已预留/准入 goal round 的取消,或其 `aborted` 结果 | `paused` | 否 |
|
||||
| 未尝试 goal round 时取消 | 保留持久 phase;撤销激活 | 否 |
|
||||
| goal phase 仍为 active 且已启用续行时的 `completed` | 准入下一 Round;达到上限时以代码 `round-limit` 阻塞 | 是 |
|
||||
| 已预留/准入 Goal Round 的取消,或其 `aborted` 结果 | `paused` | 否 |
|
||||
| 未尝试 Goal Round 时取消 | 保留持久 phase;撤销激活 | 否 |
|
||||
| `error` 且带 `RATE_LIMIT` 或 `QUOTA` | 设为 `blocked`,代码为 `usage-limited` | 否 |
|
||||
| 其他 `error`、`max-tokens` 或非陈旧提示词拒绝 | 以诊断代码和消息设为 `blocked` | 否 |
|
||||
| 持久性失败、资源释放、中断或未知未来结果 | 撤销激活或阻塞,以便检查 | 否 |
|
||||
| 持久性失败、dispose(资源释放)、中断或未知未来结果 | 撤销激活或阻塞,以便检查 | 否 |
|
||||
|
||||
某个 goal 在自身 round 中发生的变更,会取代旧 revision 的结算。因此,即使物理轮次随后关闭,完成、暂停、阻塞和编辑仍具有最终决定权。任何异常结果都不会自动重试。
|
||||
某个 goal 在自身 Round 中发生的变更,会取代旧 revision 的结算。因此,即使物理轮次随后关闭,完成、暂停、阻塞和编辑仍具有最终决定权。任何异常结果都不会自动重试。
|
||||
|
||||
## 生命周期与持久性
|
||||
|
||||
`goal/changed` 会产生持久性义务。排队工作前,驱动器会等待 `ctx.sessions.flush()`,并在等待后重新检查 goal revision 与竞争输入。关闭时的 flush 失败通过 `agent/error` 到达;即使后续一次性注入已经追加另一轮次,驱动器仍会把失败关联到完全相同的已关闭轮次,然后撤销激活,避免另一 round 启动。
|
||||
`goal/changed` 会产生持久性义务。排队工作前,驱动器会等待 `ctx.sessions.flush()`,并在等待后重新检查 goal revision 与竞争输入。关闭时的 flush 失败通过 `agent/error` 到达;即使后续一次性注入已经追加另一轮次,驱动器仍会把失败关联到完全相同的已关闭轮次,然后停用续行,避免另一 Round 启动。
|
||||
|
||||
此插件加载到现有 agent 上时绝不会继承激活状态。`GoalService.disarm()` 会移除进程本地权限,而不改变持久 phase、revision 或历史;之后由用户明确授权的 resume 会记录重新激活。会话 resume 和 fork 后,goal 领域通过 `agent/session-start` 处理应用相同规则。
|
||||
此插件加载到现有 agent 上时绝不会继承续行启用状态。`GoalService.disarm()` 会移除进程本地权限,而不改变持久 phase、revision 或历史;之后由用户明确授权的 resume 会记录重新启用续行。会话 resume 和 fork 后,goal 领域通过 `agent/session-start` 处理应用相同规则。
|
||||
|
||||
取消采用先观察、后行动的顺序:具体循环会在清空队列或中止轮次前,发送带类型 cause 的 `agent/cancel-requested`。只有取消操作拥有已预留或已准入的 goal 尝试时,插件才会持久暂停 active goal;取消无关用户工作只会撤销进程本地续行权限。如果 pause 变更失败,驱动器会回退到撤销激活。插件 teardown 会关闭准入,撤销所有活跃 goal 的激活,以 `parent` cause 取消已经准入的 round,并在事件隔离仍安装的情况下等待驱动器和 agent 完全停稳。
|
||||
取消采用先观察、后行动的顺序:具体循环会在清空队列或中止轮次前,发送带类型 cause 的 `agent/cancel-requested`。仅当取消操作所针对的是已预留或已准入的 Goal Round 尝试时,插件才会持久暂停 active goal;取消无关用户工作只会撤销进程本地续行权限。如果 pause 变更失败,驱动器会回退到停用续行。插件 teardown 会关闭准入,停用所有活跃 goal 的续行,以 `parent` cause 取消已经准入的 Round,并在事件隔离仍安装的情况下等待驱动器和 agent 完全停稳。
|
||||
|
||||
## 模型体验
|
||||
|
||||
### Goal-round 提示词
|
||||
### Goal Round 提示词
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
每个已准入 round 都是一段保留的用户角色 `<goal_round>` 块,其中点明完整目标与正 round 编号。更早的用户消息、goal 状态快照、assistant 输出与工具记录仍保留在同一会话历史中。
|
||||
每个已准入 Round 都是一段保留的用户角色 `<goal_round>` 块,其中点明完整目标与正数 Round 编号。更早的用户消息、goal 状态快照、assistant 输出与工具记录仍保留在同一会话历史中。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
每个已准入 round 会增加一个固定指令块和目标。后续请求会重新发送保留的 round,直到压缩将其遮蔽;不会创建新 agent,也不会复制对话前缀。
|
||||
每个已准入 Round 会增加一个固定指令块和目标。后续请求会重新发送保留的 Round,直到压缩(compaction)将其遮蔽;不会创建新 agent,也不会复制对话前缀。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
在一个 epoch 内仅追加:每个已准入 round 都会在可复用前缀后扩展现有对话。压缩可能替换派生历史后缀,并移动可复用边界。
|
||||
在一个 epoch 内仅追加:每个已准入 Round 都会在可复用前缀后扩展现有对话。压缩可能替换派生历史后缀,并移动可复用边界。
|
||||
|
||||
## 已知限制与暂缓工作
|
||||
|
||||
- **没有独立评估器**:面向模型的 goal 策略会判断证据是否足以完成,以及 blocker 在语义上是否未变;评估器支持的认证仍保持暂缓。
|
||||
- **只在同一会话执行**:此包有意不 spawn 新 agent、不 fork 会话前缀,也不实现 Ralph 风格的独立尝试;该工作流属于自己的插件层。
|
||||
- **已接受队列的卸载竞态**:Cordis 插件卸载是异步的。已经被 agent inbox 接受的 goal 提示词可以在卸载开始前启动并消耗其 round;teardown 随后会取消请求、撤销 goal 激活并等待完全停稳。不会再启动后续 round。
|
||||
- **只有 round 上限,不是资源预算**:token、货币、时间与提供方配额策略保持独立;观察到 `RATE_LIMIT` 和 `QUOTA` 时,只会映射为阻塞原因代码 `usage-limited`。
|
||||
- **异常情况不自动重试**:短暂的提供方与持久化失败需要之后由用户授权 resume,而不是隐式重试策略。
|
||||
- **只在同一会话执行**:此包(package)有意不 spawn 新 agent、不 fork 会话前缀,也不实现 Ralph 风格的独立尝试;该工作流属于单独的插件层。
|
||||
- **已接受队列的卸载竞态**:Cordis 插件卸载是异步的。已经被 agent inbox 接受的 goal 提示词可以在卸载开始前启动并消耗其 Round;teardown 随后会取消请求、撤销 goal 激活并等待完全停稳。不会再启动后续 Round。
|
||||
- **只有 Round 上限,不是资源预算**:token、货币、时间与提供方配额策略保持独立;观察到 `RATE_LIMIT` 和 `QUOTA` 时,只会映射为阻塞原因代码 `usage-limited`。
|
||||
- **异常情况不自动重试**:暂时性的提供方与持久化失败需要之后由用户授权 resume,而不会采用隐式重试策略。
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
事件溯源的同会话目标状态。该服务在 agent(智能体)的现有会话中保留一个当前完成目标,同时将继续执行的权限作为进程本地激活状态。[goal 领域 Agent Note](../../../.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md)负责设计理由;[goal 类型目录](../../../docs/core-data-structures/goal.md)记录字面数据形状。
|
||||
事件溯源的同会话目标状态。该服务在 agent(智能体)的现有会话中保留一个当前完成目标,同时将继续执行的权限作为进程本地续行启用状态。[goal 领域 Agent Note(agent 决策记录)](../../../.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md) 负责设计理由;[goal 类型目录](../../../docs/core-data-structures/goal.md)记录具体的数据形状。
|
||||
|
||||
## 配置
|
||||
|
||||
@@ -13,25 +13,25 @@
|
||||
defaultMaxGoalRounds: 256
|
||||
```
|
||||
|
||||
`defaultMaxGoalRounds` 必须是正安全整数。`create()` 会在提交目标前于内部物化这项部署默认值;请求级取值可以覆盖它。
|
||||
`defaultMaxGoalRounds` 必须是正的安全整数。`create()` 会在提交目标前于内部物化这项部署默认值;请求级取值可以覆盖它。
|
||||
|
||||
## 服务契约
|
||||
|
||||
`ctx.goals` 只接受以对应 id 注册的完全相同的活跃 `Agent` 实例。`get()` 返回分离的 `GoalView`;变更通过 `GoalRef { id, revision }` 比较并设置限制,并拒绝陈旧引用。服务通过生成的[服务目录](../../../docs/cordis-catalog/services.md)公开 create、edit、pause、resume、complete、block 和 clear 动词。创建默认值在内部解析。`disarm()` 是仅供生命周期使用的例外:它移除进程本地续行权限,不写入新 revision,也不发送变更事件。
|
||||
`ctx.goals` 只接受以对应 id 注册的完全相同的活跃 `Agent` 实例。`get()` 返回与内部状态脱离的 `GoalView`;变更通过 `GoalRef { id, revision }` 比较并设置屏障,并拒绝陈旧引用。服务通过生成的[服务目录](../../../docs/cordis-catalog/services.md)公开 create、edit、pause、resume、complete、block 和 clear 动词。创建默认值在内部解析。`disarm()` 是仅供生命周期使用的例外:它移除进程本地续行权限,不写入新 revision,也不发出变更。
|
||||
|
||||
最多只有一个当前目标。创建操作会生成 revision 为 1 的活跃目标并将其激活。未完成的目标必须编辑、转换或清除;已完成目标可以由拥有全局新 id 的目标替换。编辑会保留 phase、blocker reason 与 activation。暂停、完成、阻塞和清除都会撤销激活。阻塞会记录策略自有的 lower-kebab-case 代码和规范化的自由文本说明;提供方限制、配置预算、执行错误与请求用户输入都使用这一种持久 phase,不会扩增生命周期状态。只有配置的 round 上限仍有剩余容量时,resume 才接受已停止 phase 或撤销激活的 active 目标;它会清除原 blocker reason。活跃且已激活的目标会拒绝冗余操作。
|
||||
最多只有一个当前目标。创建操作会生成 revision 为 1、phase 为 active 的目标并启用续行。未完成的目标必须编辑、转换或清除;已完成目标可以由拥有全局未使用过的 id 的目标替换。编辑会保留 phase、blocker reason 与 activation。暂停、完成、阻塞和清除都会停用续行。阻塞会记录策略自有的 lower-kebab-case 代码和规范化的自由文本说明;提供方限制、配置预算、执行错误与请求人工输入都使用这一种持久 phase,不会扩增生命周期状态。只有配置的 Round 上限仍有剩余容量时,resume 才接受已停止 phase 或phase 为 active 但已停用续行的目标;它会清除原 blocker reason。phase 为 active 且已启用续行的目标会拒绝冗余操作。
|
||||
|
||||
每次非 clear 变更都会通过 `agent.inject()` 追加完整的版本化快照;clear 则追加带 revision 的 tombstone。模型可见的 `user/message` 内容与其带类型的 `{ kind: 'goal', change }` 来源必须完全一致。回放会拒绝形状错误、来源/内容漂移、不连续 revision、非法生命周期转换、每目标时间戳非单调,以及不连续的 goal round。墙上时间倒退时,变更时间戳会限制在不早于上一次目标更新的值。
|
||||
每次非 clear 变更都会通过 `agent.inject()` 追加完整的版本化快照;clear 则追加带 revision 的 tombstone。模型可见的 `user/message` 内容与其带类型的 `{ kind: 'goal', change }` 来源必须完全一致。回放会拒绝形状错误、来源/内容漂移、不连续 revision、非法生命周期转换、每目标时间戳非单调,以及不连续的 Goal Round。墙钟时间倒退时,变更时间戳会限制在不早于上一次目标更新的值。
|
||||
|
||||
注入可以立即追加,也可能在活跃工具批次 FIFO 中等待。服务会在内存中覆盖已经接受但尚待写入的变更,并在每个确切载荷进入日志时执行协调,因此连续的模型工具变更可以看到自身最新 revision,而不会把尚未记录的缓存当作持久状态。可重入追加观察者会且只会看到每项已接受变更一次;增量回放会把游标保留在第一个损坏事件处。追加或入队成功后才触发 `goal/changed`;监听器失败会受到隔离。
|
||||
注入可以立即追加,也可能在活跃工具批次 FIFO 中等待。服务会在内存中叠加已接受的待处理变更,并在每个完全一致的载荷进入日志时逐一完成对账,因此连续的模型工具变更可以看到自身最新 revision,而不会把尚未记录的缓存当作持久状态。可重入追加观察者会且只会看到每项已接受变更一次;增量回放会把游标保留在第一个损坏事件处。追加或入队成功后才触发 `goal/changed`;监听器失败会被隔离处理。
|
||||
|
||||
激活状态绝不持久化。新缓存与每条 `agent/session-start` 边都会撤销激活,即使回放找到了持久 phase 为 active 的目标。续行驱动器在卸载前或持久性不确定后也会调用 `disarm()`。因此,会话恢复、fork 与驱动器替换会保留目标、phase、revision 和已准入 round 数量,却不会启动工作;之后必须通过显式 resume 变更重新激活续行。
|
||||
续行启用状态绝不持久化。新缓存与每次触发 `agent/session-start` 时都会停用续行,即使回放找到了持久 phase 为 active 的目标。续行驱动器在卸载前或持久性不确定后也会调用 `disarm()`。因此,会话恢复、fork 与驱动器替换会保留目标、phase、revision 和已准入 Round 数量,却不会启动工作;之后必须通过显式 resume 变更重新启用续行。
|
||||
|
||||
单独发布的 `./invariant` 配套模块会为每个已挂接会话维护独立折叠。它会在候选事件进入持久日志前拒绝错误的 goal 来源变更、模型可见内容漂移、不连续 revision、非法生命周期转换、时间戳回退,以及不连续的已准入 round。
|
||||
单独发布的 `./invariant` 配套模块会为每个已挂接会话维护独立折叠。它会在候选事件进入持久日志前拒绝格式错误的 goal 来源变更、模型可见内容漂移、不连续 revision、非法生命周期转换、时间戳回退,以及不连续的已准入 round。
|
||||
|
||||
## 扩展点
|
||||
|
||||
策略插件调用服务动词,并响应限定范围的 `goal/changed` 事件。续行消费方将 round 准入为 `user/message` 事件,并携带 `GoalMessageSource`;普通用户轮次绝不会增加 `roundsStarted`。消费方使用 `Agent` 接口和事件,不导入 `dsh-agent-loop`。
|
||||
策略插件调用服务动词,并响应限定范围的 `goal/changed` 事件。续行消费方将 Round 准入为 `user/message` 事件,并携带 `GoalMessageSource`;普通的人类轮次绝不会增加 `roundsStarted`。消费方使用 `Agent` 接口和事件,不导入 `dsh-agent-loop`。
|
||||
|
||||
## 模型体验
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
#### Token 影响
|
||||
|
||||
每项保留的变更都会向派生历史增加一份完整快照,直到压缩将其遮蔽。完整快照让每条记录都能独立检查,但会重复目标和生命周期字段。
|
||||
每项保留的变更都会向派生历史增加一份完整快照,直到压缩(compaction)将其遮蔽。完整快照让每条记录都能独立检查,但会重复目标和生命周期字段。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
@@ -51,8 +51,8 @@
|
||||
|
||||
## 已知限制与暂缓工作
|
||||
|
||||
- **只负责状态,不负责任务调度**:此包不决定已激活目标何时继续,不重试异常失败,也不取消活跃轮次;这些策略属于 agent seam 消费方。
|
||||
- **只有 round 数量预算**:`maxGoalRounds` 不计量 token、货币、墙上时间或提供方配额。
|
||||
- **只负责状态,不负责任务调度**:此包(package)不决定已启用续行的目标何时继续,不重试异常失败,也不取消活跃轮次;这些策略属于 agent seam 消费方。
|
||||
- **只有 Round 数量预算**:`maxGoalRounds` 不计量 token、货币、墙钟时间或提供方配额。
|
||||
- **没有独立评估器**:记录完成或阻塞的调用方拥有最终决定权;由评估器支持的认证暂缓到独立策略层。
|
||||
- **只有一个当前目标**:系统有意不支持并行目标或独立目标数据库;替换或清除后,历史仍可在会话日志中读取。
|
||||
- **信任进程内生产方**:能直接访问 `Session` 的插件可以追加伪造的 goal 来源数据。严格回放会检测错误或不一致的记录,并使 goal 访问从该记录起失败,直到日志修复;这是完整性检测,不是插件隔离。
|
||||
- **信任进程内生产方**:能直接访问 `Session` 的插件可以追加伪造的 goal 来源数据。严格回放会检测格式错误或不一致的记录,并使 goal 访问从该记录起失败,直到日志修复;这是完整性检测,不是插件隔离。
|
||||
|
||||
@@ -2,27 +2,27 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
[`ctx.goals`](../goal/README.md) 的面向模型控制接口:`get_goal`、`create_goal` 和 `update_goal`。[goal 工具 Agent Note](../../../.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.md)负责权限拆分与 Codex 风格用户体验。
|
||||
[`ctx.goals`](../goal/README.md) 的面向模型控制接口:`get_goal`、`create_goal` 和 `update_goal`。[goal 工具 Agent Note(agent 决策记录)](../../../.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.md) 负责权限拆分与 Codex 风格用户体验。
|
||||
|
||||
## 工具
|
||||
|
||||
- `get_goal()` 返回当前 goal 或 `null`,包括比较并设置 id/revision、持久 phase、已经准入/受限的 goal round、任何 blocker reason,以及当前进程本地激活状态。
|
||||
- `create_goal(objective, max_goal_rounds?)` 从顶层用户直接轮次创建一个 goal。模型可以从长期 goal 意图中推断,而无需精确命令短语;非用户轮次和 subagent 会在执行时被拒绝。
|
||||
- `get_goal()` 返回当前 goal 或 `null`,包括比较并设置 id/revision、持久 phase、Goal Round 的已准入数/上限、任何 blocker reason,以及当前进程本地续行启用状态。
|
||||
- `create_goal(objective, max_goal_rounds?)` 根据顶层人类直接发起的轮次创建一个 goal。模型可以推断长期运行的 goal 意图,而无需精确命令短语;非人类轮次和 subagent 会在执行时被拒绝。
|
||||
- `update_goal(goal_id, revision, action, objective?, max_goal_rounds?, blocked_reason?)` 支持 `edit`、`pause`、`resume`、`complete` 和 `blocked`。替换值只属于 `edit`;`blocked_reason` 只有在 action 为 `blocked` 时才必填,并以稳定代码 `model-reported` 持久化。严格 schema 下的空字符串和零填充值视为省略,而有意义的值仍限定到各自 action。
|
||||
|
||||
所有调用都互斥,因此模型排序的批次能观察到更早变更及其新 revision。UI 客户端会收到纯通用卡片:`get_goal` 使用 read,变更使用 other。变更卡片选择第一个有意义的 action 值,否则显示 goal id,因此已接受的填充值绝不会产生空输入。
|
||||
|
||||
3 个规范值都与已经渲染给 Native 调用方的紧凑 JSON 一致:`{ goal: null }` 或 `{ goal: { id, revision, objective, phase, roundsStarted, maxGoalRounds, blockedReason? }, activation }`。因此,编程消费方无需解析渲染后的 JSON,即可收到相同领域结构。
|
||||
|
||||
自主 goal round 成功报告 `complete` 或 `blocked` 时,会用 `concludeTurn()` 标记该次工具执行,使物理轮次在该步骤后停止。用户直接变更绝不会贡献该停止决策:assistant 可以确认变更,并发的用户 steering 仍可进入循环。
|
||||
自主 Goal Round 成功报告 `complete` 或 `blocked` 时,会用 `concludeTurn()` 标记该次工具执行,使物理轮次在该步骤后停止。人类直接变更绝不会导致这种停止:assistant 可以确认变更,循环仍可接收并发的人类 steering(中途引导)。
|
||||
|
||||
## 权限
|
||||
|
||||
执行要求完全相同的活跃 `exec.agent`、其继承的 `AgentRegistry` initiator、running 状态与开放轮次。create、edit、pause 和 resume 还要求运行时根 agent 的当前轮次中存在已接受的 `{ kind: 'user' }` 消息或 steering 事件。持久 fork 谱系不会降低已恢复根 agent 的等级;活跃 subagent 所有权会降低。
|
||||
|
||||
`{ kind: 'user' }` 是宿主证明。`Agent.followup()` 与 `steer()` 会在调用方省略 source 时分配该值,因此插件、调度器与其他非用户生产方必须传入自己的 source,不能继承用户权限。
|
||||
`{ kind: 'user' }` 是宿主证明。`Agent.followup()` 与 `steer()` 会在调用方省略 source 时分配该值,因此插件、调度器与其他非人类生产方必须传入自己的 source,不能继承用户权限。
|
||||
|
||||
complete 与 blocked 还接受完全相同的当前 goal round:来源为 goal 的 `user/message`,其 id、revision 和 round 与折叠后的当前 goal 相等。在达到 `blockedAfterConsecutiveRounds` 前,goal-round 的 blocked 调用会被机械拒绝;模型判断同一条件是否确实持续,并必须在 `blocked_reason` 中说明。用户直接权限可以立即停止 goal。
|
||||
complete 与 blocked 还接受完全一致的当前 Goal Round:来源为 goal 的 `user/message`,其 id、revision 和 Round 编号与折叠后的当前 goal 相等。在达到 `blockedAfterConsecutiveRounds` 前,Goal Round 的 blocked 调用会被机械拒绝;模型判断同一条件是否确实持续,并必须在 `blocked_reason` 中说明。人类直接授权可以立即停止 goal。
|
||||
|
||||
## 配置
|
||||
|
||||
@@ -33,7 +33,7 @@ complete 与 blocked 还接受完全相同的当前 goal round:来源为 goal
|
||||
blockedAfterConsecutiveRounds: 3
|
||||
```
|
||||
|
||||
该值必须是正安全整数。它既提供模型自行阻塞的硬下限,也决定模型指引中点名的数量。
|
||||
该值必须是正的安全整数。它既提供模型自行报告阻塞的硬下限,也决定模型指引中指明的数值。
|
||||
|
||||
## 模型体验
|
||||
|
||||
@@ -41,7 +41,7 @@ complete 与 blocked 还接受完全相同的当前 goal round:来源为 goal
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
固定 goal 策略说明何种用户语义意图值得创建 goal,要求更新前先精确读取 ref,解释会话 resume/fork 后如何重新激活,并限制完成/阻塞声明。配置的阈值会插入该指引。
|
||||
固定 goal 策略说明何种用户语义意图值得创建 goal,要求更新前先精确读取 ref,解释会话 resume/fork 后如何重新启用续行,并限制完成/阻塞声明。配置的阈值会插入该指引。
|
||||
|
||||
##### Goal 策略
|
||||
|
||||
@@ -55,26 +55,26 @@ Use goal tools for one long-running completion objective in the current session.
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
插件范围、配置阈值和指引文本不变时,前缀保持稳定。激活、资源释放或配置变更可能使此提示词章节的复用失效。
|
||||
插件范围、配置阈值和指引文本不变时,前缀保持稳定。启用、dispose(资源释放)或配置变更可能使此提示词章节的复用失效。
|
||||
|
||||
### 工具 schema 与结果
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
生成的 [`get_goal`、`create_goal` 和 `update_goal` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-goal)。成功结果是紧凑 JSON。变更结果之后是工具批次结束后由 goal 领域产生的原始 `<goal_state>` 快照。结果中的 `activation` 是活跃观察值,绝不会成为回放权限依据。
|
||||
生成的 [`get_goal`、`create_goal` 和 `update_goal` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-goal)。成功结果是紧凑 JSON。变更结果之后是工具批次结束后由 goal 领域产生的原始 `<goal_state>` 快照。结果中的 `activation` 是实时观察值,绝不会成为回放权限依据。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
固定 schema 成本,加上每次调用的一条紧凑结果。变更还会保留领域快照,直到压缩。
|
||||
固定 schema 成本,加上每次调用的一条紧凑结果。变更还会保留领域快照,直到压缩(compaction)。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
Schema 的定义与可见性不变时,前缀保持稳定。调用、结果和生成的 goal 快照会追加到可复用请求前缀之后,不会使更早条目失效。
|
||||
schema 的定义与可见性不变时,前缀保持稳定。调用、结果和生成的 goal 快照会追加到可复用请求前缀之后,不会使更早条目失效。
|
||||
|
||||
## 已知限制与暂缓工作
|
||||
|
||||
- **语义意图仍由模型判断**:执行只能证明直接用户来源,无法证明请求是否足够重大而值得创建 goal。
|
||||
- **阻塞条件是否相同仍由模型判断**:运行时强制执行不同的已准入 round 计数,而不是障碍的语义等价性;独立评估器保持暂缓。
|
||||
- **不负责调度或直接用户呈现**:这些工具只变更状态;同会话驱动器与 [`dsh-command-goal`](../command-goal/README.md) 是同一领域的独立消费方。
|
||||
- **Goal-round 权限需要驱动器**:除非续行驱动器准入 goal 来源的用户轮次,否则自主 `complete`/`blocked` 路径不会启用;只挂载此工具包不会创建这些轮次。
|
||||
- **语义意图仍由模型判断**:执行只能证明人类直接来源,无法证明请求是否足够重大而值得创建 goal。
|
||||
- **阻塞条件是否相同仍由模型判断**:运行时强制统计互不重复的已准入 Goal Round,而不判断障碍在语义上是否等价;独立评估器的实现暂缓。
|
||||
- **不负责调度或直接面向人类呈现**:这些工具只变更状态;同会话驱动器与 [`dsh-command-goal`](../command-goal/README.md) 是同一领域的独立消费方。
|
||||
- **Goal Round 权限需要驱动器**:除非续行驱动器准入 goal 来源的用户轮次,否则自主 `complete`/`blocked` 路径不会启用;只挂载这个包(package)不会创建这些轮次。
|
||||
- **提示词注册与过滤相互独立**:某个范围可能隐藏工具,却保留指引,除非部署将两项注册限定在同一范围。
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
这组行为 guard 插件会监视 agent(智能体)循环中的无效模式,并提醒模型调整方向。这里只有一个**产品** 包,不设接口/实现 seam:guard 是现有核心 seam(`tools/post-execute`、`agent/prompt-submit`、`agent/status`)的自包含消费方,并非可替换能力。
|
||||
这组行为 guard 插件会监视 agent loop(智能体循环)中的低效模式,并提醒模型调整方向。这里只有一个**产品**包(package),不设接口/实现 seam:guard 是现有核心 seam(`tools/post-execute`、`agent/prompt-submit`、`agent/status`)的自包含消费方,并非可替换能力。
|
||||
|
||||
| 包 | 职责 | ctx 键 |
|
||||
|---|---|---|
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
这是一个仅提供建议的循环中断器,而非面向模型的工具:它不会出现在工具列表中,不会否决或改写调用,只增加一种行为。它监视每个 agent(智能体)的工具调用流,统计以完全相同的规范化参数连续调用同一工具的次数;达到所配置的连续次数时,它会注入逐级增强的提示,要求模型停止重复、重新阅读上一次结果,并改用其他方案或结束任务。究竟是换一种方式重试、收集更多证据还是完成任务,仍完全由模型决定:合理的重复调用既不会延迟,也不会受阻。决策记录见 [repeat-tool-guard Agent Note](../../../.agents/notes/archived/feature/2026-07-08-repeat-tool-guard.md)。
|
||||
这是一个仅提供建议的循环中断器,而非面向模型的工具:它不会出现在工具列表中,不会否决或改写调用,只增加一种行为。它监视每个 agent(智能体)的工具调用流,统计以完全相同的规范化参数连续调用同一工具的次数;达到所配置的连续次数时,它会注入逐级增强的提示,要求模型停止重复、重新阅读上一次结果,并改用其他方案或结束任务。究竟是换一种方式重试、收集更多证据还是完成任务,仍完全由模型决定:合理的重复调用既不会延迟,也不会受阻。决策记录见 [repeat-tool-guard Agent Note(agent 决策记录)](../../../.agents/notes/archived/feature/2026-07-08-repeat-tool-guard.md)。
|
||||
|
||||
## 配置
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
argumentsPreviewChars: 500 # default; cap on arguments quoted in the detailed reminder
|
||||
```
|
||||
|
||||
插件加载时,`thresholds` 会对错误配置快速失败:空列表、非整数、小于 2 的值或重复值都会抛出错误,绝不静默回退到默认值;`argumentsPreviewChars` 同样只接受大于等于 1 的整数。系统会将列表按升序规范化;第一个阈值只发送简短的通用提醒,后续每个阈值都会发送详细版本,列出工具、连续次数和规范参数。参数内容在 `argumentsPreviewChars` 处从头截断,并附带省略字符数标记,避免循环中的 `write`/`edit` 载荷无限制进入下一次请求(链键始终比较完整的规范字符串;此上限只约束提醒,不影响检测)。
|
||||
插件加载时,`thresholds` 会对错误配置快速失败:空列表、非整数、小于 2 的值或重复值都会抛出错误,绝不静默回退到默认值;`argumentsPreviewChars` 同样只接受大于等于 1 的整数。系统会将列表按升序规范化;第一个阈值只发送简短的通用提醒,后续每个阈值都会发送详细版本,列出工具、连续次数和规范参数。参数内容截取前 `argumentsPreviewChars` 个字符,并附带省略字符数标记,避免循环中的 `write`/`edit` 载荷无限制进入下一次请求(链键始终比较完整的规范字符串;此上限只约束提醒,不影响检测)。
|
||||
|
||||
`include`/`exclude` 条目支持 `*` 通配符,并针对调用时实际存在的工具执行谓词判断,而不是引用注册表条目。因此,与当前任何已注册工具都不匹配的模式并非错误(未加载 MCP 工具的部署中,`exclude: [mcp_*]` 仍然有效);这与 `toolOrder` 的引用目标检查不同。
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
- **不受跟踪的调用对链透明。** 被 `include`/`exclude` 排除的调用既不递增计数器,也不重置计数器;因此,`grep X → todo_write → grep X` 仍算作连续两次 `grep X`,即使 `todo_write` 已被排除。这正是排除机制的价值:循环中穿插的记录类工具不能掩盖循环。
|
||||
- **被拒绝的调用也计数。** 检测位于 `tools/post-execute`;即便调用被 `tools/pre-execute` 监听器拒绝,该事件也会运行。模型反复尝试被拒绝的调用,恰恰是需要打断的循环。
|
||||
- **忽略没有 agent 的调用。** 直接调用 `ctx.tools.execute()` 的调用方没有需要提醒的模型,也没有可作为键的活跃 agent 对象。
|
||||
- **按 agent 分键。** 工具注册表位于上下文层级,subagent 会交错通过同一个 waterfall,因此每条链使用 `WeakMap<Agent, Chain>`,以活跃 agent 对象为键。一个 agent 的重复调用绝不会触发另一个 agent 的提醒。用户提示词(`agent/prompt-submit`)会重置提交该提示词的 agent 链;对象生命周期会自然限制弱引用条目的寿命,无需资源释放监听器。
|
||||
- **仅驻留内存。** 从持久化恢复的会话会从一条全新的链开始:guard 是启发式提醒,并非有日志记录的不变量;这是接受的代价,即后续提醒可能重新开始。
|
||||
- **按 agent 分键。** 工具注册表位于上下文层级,subagent 会交错通过同一个 waterfall(瀑布式事件),因此每条链使用 `WeakMap<Agent, Chain>`,以活跃 agent 对象为键。一个 agent 的重复调用绝不会触发另一个 agent 的提醒。用户提示词(`agent/prompt-submit`)会重置提交该提示词的 agent 链;对象生命周期会自然限制弱引用条目的寿命,无需 dispose(资源释放)监听器。
|
||||
- **仅驻留内存。** 从持久化恢复的会话会从一条全新的链开始:guard 是启发式提醒,并非有日志记录的不变量;提醒会延后,这是可接受的代价。
|
||||
|
||||
## 提醒传递
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
## 测试
|
||||
|
||||
单元测试使用 mock 适配器(无网络)驱动真实 agent loop,并对上述链语义实现逐文件 100% 覆盖率。快照层负责 transcript(文本记录)接口:脚本化回放场景会将同一调用重复 5 次,并在 ACP transcript 中固定两个提醒层级,即第 3 次的温和提醒和第 5 次的详细提醒;二者均为注入的 `user/message`。
|
||||
单元测试使用 mock 适配器(无网络)驱动真实 agent loop,并对上述链语义实现逐文件 100% 覆盖率。快照层负责 transcript(文本记录)接口:脚本化回放场景会将同一调用重复 5 次,并在 ACP(Agent Client Protocol)的 transcript 中固定两个提醒层级,即第 3 次的温和提醒和第 5 次的详细提醒;二者均为注入的 `user/message`。
|
||||
|
||||
## 模型体验
|
||||
|
||||
@@ -87,7 +87,7 @@ The repeated calls are not making progress. Do not call this tool with these exa
|
||||
## 已知限制与暂缓工作
|
||||
|
||||
- **仅检测精确匹配**:规范化过程会对键进行深度排序,因此近似变体(稍作修改的路径、值内增加的空白)可以绕过链;在没有需求证据前,不采用模糊匹配。
|
||||
- **压缩不会重置链**:跨越压缩检查点的链会继续计数。
|
||||
- **压缩(compaction)不会重置链**:跨越压缩检查点的链会继续计数。
|
||||
- **仅提供建议**:尚未实现达到较高阈值后升级为 `block`,但 `PostToolDecision` 已支持阻止调用。
|
||||
- **subagent 之间不共享链**:链始终按 agent 隔离;即使父 agent 与其 subagent 重复相同调用,也不会合并计数。
|
||||
- **合理的幂等轮询超过阈值后仍会收到提醒**:可通过 `thresholds`/`exclude` 配置释放压力。
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
hooks 子系统让用户可以像使用 Claude Code 和 Codex 一样,在 agent 生命周期节点扩展 agent:把桥接插件指向现有的 `hooks.json`(或 settings),即可忠实运行这些外部 shell hook。规范的扩展表层本身是 harness 的类型化拦截 seam(见[拦截 seam Agent Note](../../.agents/notes/implemented/feature/2026-06-30-interception-seams.md));「原生 hook」只是这些 seam 上的普通 cordis 插件。这些包是把外部 shell-hook 协议转换到同一表层的**桥接**,另含它们共同依赖的共享协议格式库。
|
||||
hooks 子系统让用户可以像使用 Claude Code 和 Codex 一样,在 agent(智能体)生命周期节点扩展 agent:把桥接插件指向现有的 `hooks.json`(或 settings),即可忠实运行这些外部 shell hook。规范的扩展表层本身是 harness 的类型化拦截 seam(见[拦截 seam Agent Note(agent 决策记录)](../../.agents/notes/implemented/feature/2026-06-30-interception-seams.md));「原生 hook」只是这些 seam 上的普通 Cordis 插件。这些包是把外部 shell-hook 协议转换到同一表层的**桥接**,另含它们共同依赖的共享协议格式(wire format)库。
|
||||
|
||||
| 包 | 职责 | 形态 |
|
||||
|---|---|---|
|
||||
@@ -10,4 +10,4 @@ hooks 子系统让用户可以像使用 Claude Code 和 Codex 一样,在 agent
|
||||
| `hooks-claude/` | Claude Code `hooks.json`/settings 的桥接 | 插件 |
|
||||
| `hooks-codex/` | Codex `hooks.json` 的桥接 | 插件 |
|
||||
|
||||
Codex 有意重新实现 Claude Code 协议的一个*子集*(`hooks.json` 形状相同、5 个事件而非 CC 的众多事件、仅命令、仅正则表达式 matcher、没有 env/替换),因此 `hook-protocol` 拥有真正相同的原语,每个桥接只拥有不同部分(逐事件 stdin 载荷、env,以及把 hook 的中性结果映射到 harness 类型化 Decision 的方式)。参见 [hook-protocol/README.md](hook-protocol/README.md)。
|
||||
Codex 有意重新实现 Claude Code 协议的一个*子集*(`hooks.json` 结构相同、5 个事件而非 CC 的众多事件、仅命令、仅正则表达式 matcher、没有 env/替换),因此 `hook-protocol` 负责真正相同的原语,每个桥接只负责不同部分(逐事件 stdin 载荷、env,以及把 hook 的中性结果映射到 harness 类型化 Decision 的方式)。参见 [hook-protocol/README.md](hook-protocol/README.md)。
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
Claude Code/Codex hook 协议格式的**共享核心**。它不是 cordis 插件:不注册也不注入任何内容。它是一个**库**,提供两个桥接插件(`@deepseek-ai/dsh-hooks-claude`、`@deepseek-ai/dsh-hooks-codex`)导入的方言无关原语,使两者都无需重复实现协议中相同的部分。
|
||||
Claude Code/Codex hook 协议格式(wire format)的**共享核心**。它不是 Cordis 插件:不注册也不注入任何内容。它是一个**库**,提供两个桥接插件(`@deepseek-ai/dsh-hooks-claude`、`@deepseek-ai/dsh-hooks-codex`)导入的方言无关原语,使两者都无需重复实现协议中相同的部分。
|
||||
|
||||
共享 lib 存在的原因是:Codex 有意重新实现了 Claude Code hook 协议的一个*子集*,包括相同的 `hooks.json` matcher group 形状、相同的退出码/stdout 输出契约以及相同的 command hook 执行模式。真正共享的部分位于此处;每个桥接只拥有不同之处。
|
||||
共享 lib 存在的原因是:Codex 有意重新实现了 Claude Code hook 协议的一个*子集*,包括相同的 `hooks.json` matcher group 结构、相同的退出码/stdout 输出契约以及相同的 command hook 执行模式。真正共享的部分位于此处;每个桥接只负责不同的部分。
|
||||
|
||||
## 共享内容(此处)与每方言内容(桥接)
|
||||
## 共享内容(此处)与各方言内容(桥接)
|
||||
|
||||
| 关注点 | 此处(`dsh-hook-protocol`) | 桥接(`dsh-hooks-claude` / `-codex`) |
|
||||
|---|---|---|
|
||||
@@ -15,21 +15,21 @@ Claude Code/Codex hook 协议格式的**共享核心**。它不是 cordis 插
|
||||
| 解码输出 | `parseHookOutput(exit, stdout, stderr)` → 中性 `HookOutput` | 将中性 `HookOutput` 映射到 seam 特定的类型化 Decision |
|
||||
| 合并 N 个 hook | `mergeHookOutputs(outputs)` → 最严格的 `MergedHookOutcome` | (无) |
|
||||
| 持久记录 | `appendHookInvoked` / `appendHookResult`(`hook/*` 会话事件;结果的 `decision`/`stderrSummary` 从此处的 `HookOutput` 派生) | 在每次调用前后调用它们 |
|
||||
| 脱离运行完全停稳 | `createDetachedRuns()`:跟踪发射后不再等待的运行链;`drain()` 先 abort,再等待它们 | 将 `signal` 传给每个脱离的 `runHook`,并将 `drain` 注册为 effect disposer |
|
||||
| 脱离运行完全停稳 | `createDetachedRuns()`:跟踪触发后不等待的运行链;`drain()` 先 abort,再等待它们 | 将 `signal` 传给每个脱离的 `runHook`,并将 `drain` 注册为 effect disposer |
|
||||
|
||||
## 原语
|
||||
|
||||
- **`matchesMatcher(matcher, query, mode)`**:缺失、`''` 或 `'*'` 时匹配全部;`claude` 模式将纯 `[A-Za-z0-9_|]+` pattern 视为字面值(pipe = 精确匹配交替),其他 pattern 视为正则;`codex` 模式始终使用未锚定正则。无效正则不匹配任何内容(绝不抛出异常)。
|
||||
- **`runHook(bash, hook, options, now)`**:要求并转发调用方拥有的 `options.signal`,将 `options.payload` 序列化到 hook stdin(当且仅当 `options.trailingNewline` 时添加尾随换行符),在执行器凭证清理后合并 `options.env`(`dsh-bash` 受信任插件表层),遵循 hook 的 `timeoutSec`(否则使用 `options.defaultTimeoutMs`;默认值属于桥接,其配置默认为 lib 的 `DEFAULT_HOOK_TIMEOUT_MS` 10 分钟参考值),再解码结果(将 `options.expectedEventName` 传递给 codec)。因此取消会到达执行器的进程组终止与 join 边界。它绝不抛出异常:执行器拒绝(基础设施故障)会变为 `HookOutput`,其 `exitCode: undefined`(非阻塞错误)。`now` 会被注入,以便测试持续时间。
|
||||
- **`parseHookOutput(exitCode, stdout, stderr, expectedEventName?)`** 解码退出状态与结构化 stdout。退出码 2 使用 stderr 阻塞;其他失败不阻塞。匹配的 hook 特定权限决策会覆盖遗留顶层决策;事件判别字段不匹配或缺失只会抑制事件特定字段。顶层字段仍与事件无关,成功但非 JSON 的输出会留给桥接处理。
|
||||
- **`mergeHookOutputs(outputs)`**:折叠在一个点上匹配的每个 hook 结果:权限优先级为 **deny > ask > allow**,首个 `continue:false` 使 halt 粘滞,阻塞原因用 `\n\n` 连接,`additionalContext`/`systemMessages` 按顺序累积。
|
||||
- **`createDetachedRuns()`**:为脱离运行的 emit 形状点跟踪完全停稳(没有 seam 等待它们)。桥接会跟踪每条运行链,包括 hook 运行及其 continuation,并将 `drain()` 注册为 effect disposer。drain 会触发 tracker 的 abort `signal`(因此仍在运行的 hook 进程会通过 `runHook` 终止,而不是等待到超时),随后在所有已跟踪链结算后 resolve。因此 `fiber.dispose()` resolve 时,没有脱离 hook 工作会留下并触发已 dispose 的上下文(见 [防御模式](../../../docs/defensive-patterns.md):dispose 必须达到完全停稳)。
|
||||
- **`runHook(bash, hook, options, now)`**:要求并转发调用方拥有的 `options.signal`,将 `options.payload` 序列化到 hook stdin(当且仅当 `options.trailingNewline` 时添加尾随换行符),在执行器凭证清理后合并 `options.env`(`dsh-bash` 受信任插件接口),遵循 hook 的 `timeoutSec`(否则使用 `options.defaultTimeoutMs`;默认值属于桥接,其配置默认为 lib 的 `DEFAULT_HOOK_TIMEOUT_MS` 10 分钟参考值),再解码结果(将 `options.expectedEventName` 传递给 codec)。因此取消会到达执行器的进程组终止与 join 边界。它绝不抛出异常:执行器拒绝(基础设施故障)会变为 `HookOutput`,其 `exitCode: undefined`(非阻塞错误)。`now` 会被注入,以便测试持续时间。
|
||||
- **`parseHookOutput(exitCode, stdout, stderr, expectedEventName?)`** 解码退出状态与结构化 stdout。退出码为 2 时,会以 stderr 内容阻止执行;其他失败不阻塞。匹配的 hook 特定权限决策会覆盖遗留顶层决策;事件判别字段不匹配或缺失只会抑制事件特定字段。顶层字段仍与事件无关,成功但非 JSON 的输出会留给桥接处理。
|
||||
- **`mergeHookOutputs(outputs)`**:折叠在一个点上匹配的每个 hook 结果:权限优先级为 **deny > ask > allow**,从首个 `continue:false` 起,halt 状态保持不变,阻塞原因用 `\n\n` 连接,`additionalContext`/`systemMessages` 按顺序累积。
|
||||
- **`createDetachedRuns()`**:跟踪以 emit 形式脱离运行的点是否完全停稳(没有 seam 等待它们)。桥接会跟踪每条运行链,包括 hook 运行及其 continuation,并将 `drain()` 注册为 effect disposer。drain 会触发 tracker 的 abort `signal`(因此仍在运行的 hook 进程会通过 `runHook` 终止,而不是等待到超时),随后在所有已跟踪链结算后 resolve。因此 `fiber.dispose()` resolve 时,不会遗留任何可能作用于已 dispose(资源释放)的上下文的脱离 hook 工作(见 [防御模式](../../../docs/defensive-patterns.md):dispose 必须达到完全停稳)。
|
||||
|
||||
## `hook/*` 会话事件
|
||||
|
||||
通过 declaration merging 合并到 `SessionEventMap`(仅日志,与 `compact/*` 相同;不是 `SurfaceEventType`,没有 `surfaceOp`):`hook/invoked`(hook 命令已运行)与 `hook/result`(其结果,按 `handlerId` 配对,由 `appendHookResult` 拥有决策规则)。Payload 与每事件 JSDoc 位于生成的 [持久化日志事件目录](../../../docs/persistence-catalog.md);`stderrSummary` 会截断到记录的 `stderrSummaryMaxChars`(桥接配置,参考默认值 `DEFAULT_STDERR_SUMMARY_MAX_CHARS` = 500;为空时省略)。
|
||||
通过 declaration merging 合并到 `SessionEventMap`(仅日志,与 `compact/*` 相同;不是 `SurfaceEventType`,没有 `surfaceOp`):`hook/invoked`(hook 命令已运行)与 `hook/result`(其结果,按 `handlerId` 配对,决策规则由 `appendHookResult` 负责)。Payload 与每事件 JSDoc 位于生成的 [持久化日志事件目录](../../../docs/persistence-catalog.md);`stderrSummary` 会截断到记录的 `stderrSummaryMaxChars`(桥接配置,参考默认值 `DEFAULT_STDERR_SUMMARY_MAX_CHARS` = 500;为空时省略)。
|
||||
|
||||
Hook 溯源记录必须位于开启轮次内。轮次中点(`PreToolUse`/`PostToolUse`/`Stop`)按构造满足这条由所有方定义的关系。`SessionStart` 与轮次前的 `UserPromptSubmit` 准入 seam 没有 `hook/*` 记录;获准的上下文改由其带来源的 `user/message` 作为证据,详见 hooks Agent Note。
|
||||
Hook 溯源记录必须位于一个尚未结束的轮次内。轮次中的点(`PreToolUse`/`PostToolUse`/`Stop`)按构造满足这条由所有者定义的关系。`SessionStart` 与轮次前的 `UserPromptSubmit` 准入 seam 没有 `hook/*` 记录;获准的上下文改由其带来源的 `user/message` 作为证据,详见 hooks Agent Note(agent 决策记录)。
|
||||
|
||||
## 模型体验
|
||||
|
||||
@@ -37,7 +37,7 @@ Hook 溯源记录必须位于开启轮次内。轮次中点(`PreToolUse`/`Po
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
不会直接失效;请求前缀变更由具名消费方负责。
|
||||
不会直接失效;请求前缀变更由上述消费方负责。
|
||||
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
一个 cordis 插件,在 harness 的规范拦截 seam 上运行 user 现有 **Claude Code** hook 配置(`hooks.json` 或 settings 文件的 `hooks` key)中受支持的 command hook 子集。它是 hooks 子系统的 **CC 方言** 一半,拥有桥接的 CC 形状每事件 stdin payload、CC env + `${CLAUDE_PLUGIN_ROOT}`/`${CLAUDE_PROJECT_DIR}` 替换,以及从 hook 中性结果到 harness 类型化 Decision 的映射。方言无关原语(matcher、退出码/stdout codec、`ctx.bash` 执行、最严格合并、`hook/*` 事件)来自 [`@deepseek-ai/dsh-hook-protocol`](../hook-protocol/README.md)。
|
||||
一个 Cordis 插件,在 harness 的规范拦截 seam 上运行用户现有 **Claude Code** hook 配置(`hooks.json` 或 settings 文件的 `hooks` key)中受支持的 command hook 子集。它是 hooks 子系统的 **CC 方言**部分,负责桥接中 CC 格式的逐事件 stdin payload、CC 的 env 和 `${CLAUDE_PLUGIN_ROOT}`/`${CLAUDE_PROJECT_DIR}` 替换,以及将 hook 的中性结果映射为 harness 的类型化 Decision。方言无关原语(matcher、退出码/stdout codec、`ctx.bash` 执行、最严格合并、`hook/*` 事件)来自 [`@deepseek-ai/dsh-hook-protocol`](../hook-protocol/README.md)。
|
||||
|
||||
原生 cordis 插件可以完成此桥接的所有工作,功能更强,且具有类型化返回,没有序列化边界。**该桥接只是已映射 CC command hook 子集的兼容路径**;所有定制行为都应当使用相同 seam 上的原生插件(见 [拦截 seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-30-interception-seams.md))。
|
||||
原生 Cordis 插件可以完成此桥接的所有工作,功能更强,且具有类型化返回,没有序列化边界。**该桥接只是已映射 CC command hook 子集的兼容路径**;所有定制行为都应当使用相同 seam 上的原生插件(见 [拦截 seam Agent Note(agent 决策记录)](../../../.agents/notes/implemented/feature/2026-06-30-interception-seams.md))。
|
||||
|
||||
## 配置
|
||||
|
||||
@@ -28,31 +28,31 @@ const config: Config = {
|
||||
projectDir: .
|
||||
```
|
||||
|
||||
配置只在加载时解析**一次**。`configPath` 是**进程级** 配置:相对路径在加载时根据进程启动 cwd 解析,因此一份配置应用于整个进程。尚未进行每会话(`session/new.cwd`)配置发现(`TODO(per-session-hook-config)`)。读取/解析失败会被容纳:桥接记录警告且不注册任何内容,而不是使启动崩溃(路径拼写错误不应使 agent 停止)。只运行 shell 形式 `type: 'command'` hook;`http`/`mcp_tool`/`prompt`/`agent` hook 会被解析并跳过,同时记录警告。没有每 hook `timeout` 的 hook 会使用协议参考默认值 `DEFAULT_HOOK_TIMEOUT_MS`(来自 `dsh-hook-protocol`,10 分钟,即 CC 默认值)。
|
||||
配置只在加载时解析**一次**。`configPath` 是**进程级**配置:相对路径在加载时根据进程启动 cwd 解析,因此一份配置应用于整个进程。尚未进行每会话(`session/new.cwd`)配置发现(`TODO(per-session-hook-config)`)。读取/解析失败会被隔离处理:桥接记录警告且不注册任何内容,而不是使启动崩溃(路径拼写错误不应使 agent(智能体)停止)。只运行 shell 形式 `type: 'command'` hook;`http`/`mcp_tool`/`prompt`/`agent` hook 会被解析并跳过,同时记录警告。没有每 hook `timeout` 的 hook 会使用协议参考默认值 `DEFAULT_HOOK_TIMEOUT_MS`(来自 `dsh-hook-protocol`,10 分钟,即 CC 默认值)。
|
||||
|
||||
hook **本身** 会在 agent 的会话工作区中运行:对 agent scope 点,桥接会将会话 `cwd`(`session/new.cwd`)作为 hook 进程工作目录,因此 hook 的 `pwd`/相对路径/marker 作用于 user 项目树,而非服务器启动目录。
|
||||
hook **本身**会在 agent 的会话工作区中运行:对 agent scope 点,桥接会将会话 `cwd`(`session/new.cwd`)作为 hook 进程工作目录,因此 hook 的 `pwd`/相对路径/marker 作用于 用户项目树,而非服务器启动目录。
|
||||
|
||||
## Hook 点 → seam Decision
|
||||
|
||||
| CC hook | Harness seam | 映射 |
|
||||
|---|---|---|
|
||||
| `SessionStart` | `agent/session-start`(emit) | additionalContext → `agent.inject()` 到新会话(无法阻塞) |
|
||||
| `UserPromptSubmit` | `agent/prompt-submit`(waterfall) | `deny` → `PromptDecision.block`;仅 additionalContext → 通过 `next()` 委托,再将一个单独标记源的上下文前置到下游 `additionalContexts`(后续 listener 仍可阻塞/改写) |
|
||||
| `UserPromptSubmit` | `agent/prompt-submit`(waterfall(瀑布式事件)) | `deny` → `PromptDecision.block`;仅 additionalContext → 通过 `next()` 委托,再将一个单独标记源的上下文前置到下游 `additionalContexts`(后续 listener 仍可阻塞/改写) |
|
||||
| `PreToolUse` | `tools/pre-execute`(waterfall) | `deny` → `PreToolDecision.deny`;`ask` → `PreToolDecision.ask` |
|
||||
| `PostToolUse` | `tools/post-execute`(waterfall) | `deny` → 带反馈的 `block`;仅 additionalContext → 通过 `next()` 委托,再将一个单独标记源的上下文前置到下游决策;Code Mode 将子调用上下文延迟到外层 `run_code` 结果 |
|
||||
| `Stop` | `agent/turn-stopping`(serial) | 阻塞 Stop hook 通过 `steer()` 送入其原因,强制再执行一步 |
|
||||
| `SubagentStart` | `subagent/start`(emit) | additionalContext → `agent.inject()` 到实时同进程 child;远程 child 没有本地注入目标 |
|
||||
| `SubagentStart` | `subagent/start`(emit) | additionalContext → `agent.inject()` 到仍在运行的同进程 child;远程 child 没有本地注入目标 |
|
||||
| `SubagentStop` | `subagent/end`(emit) | 只观测 |
|
||||
|
||||
三个 emit 点都脱离运行:没有 seam 会等待 `SessionStart`/`SubagentStart`/`SubagentStop` hook。每条运行链都会被跟踪;dispose 桥接会中止仍在运行的 hook 进程,再排空 continuation,然后 dispose resolve(`createDetachedRuns`,位于 `dsh-hook-protocol`)。
|
||||
三个 emit 点都以分离方式运行:没有 seam 会等待 `SessionStart`/`SubagentStart`/`SubagentStop` hook。每条运行链都会被跟踪;对桥接执行 dispose(资源释放)时,会中止仍在运行的 hook 进程,并在 dispose 完成前排空 continuation(`createDetachedRuns`,位于 `dsh-hook-protocol`)。
|
||||
|
||||
matcher subject 是工具名称(`PreToolUse`/`PostToolUse`)、会话源(`SessionStart`),或常量 `agent_type`,其值为 `general-purpose`(`SubagentStart`/`SubagentStop`)。harness subagent seam 不携带每 kind label,因此桥接报告 Claude Code 自身 Task 工具默认值;默认/`*`/空 `agent_type` matcher 会触发,特定 kind matcher 不会触发。`UserPromptSubmit`/`Stop` 忽略 matcher。一个点上文件配置的多个 hook 会**按配置顺序串行运行**,并按最严格方式折叠(`deny > ask > allow`,见 `dsh-hook-protocol`)。串行使每个 hook 的 `hook/invoked`/`hook/result` 对在日志中相邻,决策折叠与顺序无关(见 Agent Note 的「run serially, not concurrently」说明)。
|
||||
|
||||
每个 agent scope stdin payload 都携带 `session_id` 与字符串形状的 `transcript_path`。可用时,桥接通过 `ctx.sessionPersistence.locate(session.header)` 解析后者,否则发送 `''`。查找不会创建或 flush 产物,因此第一个轮次结束检查点之前路径可能不存在,也可能省略当前开启轮次。
|
||||
每个 agent scope stdin payload 都携带 `session_id` 与字符串形式的 `transcript_path`。可用时,桥接通过 `ctx.sessionPersistence.locate(session.header)` 解析后者,否则发送 `''`。查找不会创建或 flush 产物,因此第一个轮次结束检查点之前路径可能不存在,也可能省略当前开启轮次。
|
||||
|
||||
## 上下文源
|
||||
|
||||
注入上下文携带显式 `{ kind: 'plugin', plugin: 'hooks-claude' }` 源。`agent.inject()` 会将缺失源默认为 `{ kind: 'user' }`,这会将插件上下文错误标记为 user 提示词,因此桥接始终标注自身。
|
||||
注入上下文携带显式 `{ kind: 'plugin', plugin: 'hooks-claude' }` 源。`agent.inject()` 会将缺失源默认为 `{ kind: 'user' }`,这会将插件上下文错误标记为 用户提示词,因此桥接始终标注自身。
|
||||
|
||||
## 模型体验
|
||||
|
||||
@@ -60,15 +60,15 @@ matcher subject 是工具名称(`PreToolUse`/`PostToolUse`)、会话源(
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
`SessionStart`、已接受提示词、工具后和实时同进程 subagent-start hook 可以添加带源归因的上下文消息;阻塞 `Stop` hook 将原因添加为下一步 steering。远程 child 注入没有本地目标。
|
||||
`SessionStart`、已接受提示词、工具后和实时同进程 subagent-start hook 可以添加带源归因的上下文消息;阻塞 `Stop` hook 将原因添加为下一步 steering(中途引导)。远程 child 注入没有本地目标。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
hook 不返回上下文时没有成本。Hook 文本取决于数据,会被记录,并在后续会话请求中重发,直到压缩。
|
||||
hook 不返回上下文时没有成本。Hook 文本取决于数据,会被记录,并在后续会话请求中重发,直到压缩(compaction)。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
仅追加;新可见内容位于可复用请求前缀之后,不会使现有 KV-cache 配置项失效。
|
||||
仅追加;新可见内容位于可复用请求前缀之后,不会使现有 KV-cache 条目失效。
|
||||
|
||||
### 已阻塞提示词或工具结果
|
||||
|
||||
@@ -89,9 +89,9 @@ hook 不返回上下文时没有成本。Hook 文本取决于数据,会被记
|
||||
- **不支持的 hook 事件(Claude Code 当前 30 项中的 23 项):** `Setup`、`InstructionsLoaded`、`UserPromptExpansion`、`MessageDisplay`、`PermissionRequest`、`PostToolUseFailure`、`PostToolBatch`、`PermissionDenied`、`Notification`、`TaskCreated`、`TaskCompleted`、`StopFailure`、`TeammateIdle`、`ConfigChange`、`CwdChanged`、`FileChanged`、`WorktreeCreate`、`WorktreeRemove`、`PreCompact`、`PostCompact`、`SessionEnd`、`Elicitation` 和 `ElicitationResult`。这些事件的配置会被解析,但绝不分派。比较基线是 Claude Code [官方 hook 事件参考](https://code.claude.com/docs/en/hooks#hook-events)。
|
||||
- **`SessionStart` 只支持部分功能:** 会消费 JSON `additionalContext`,但不支持纯 stdout 上下文、`initialUserMessage`、`sessionTitle`、`watchPaths`、`reloadSkills` 与 `CLAUDE_ENV_FILE`。hook 脱离运行,因此上下文可能错过第一个请求(`TODO(session-start-gating)`),payload 会省略 `model`、`agent_type` 和 `session_title` 等当前可选字段。
|
||||
- **`UserPromptSubmit` 只支持部分功能:** 支持阻塞与 JSON `additionalContext`,但不支持纯 stdout 上下文、`sessionTitle` 和 `suppressOriginalPrompt`。除非被覆盖,否则桥接还会使用自身 600 秒默认值,而非 Claude Code 的事件特定 30 秒 command 超时。
|
||||
- **`PreToolUse` 只支持部分功能:** `deny` 与 `ask` 决策可用;`allow` 不会预批准,不支持 `defer`,`additionalContext` 会被忽略,`updatedInput` 会被记录 + 警告但不应用(见 [pre-tool-input-rewrite Agent Note](../../../.agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.md))。
|
||||
- **`PreToolUse` 只支持部分功能:** `deny` 与 `ask` 决策可用;`allow` 不会预审批,不支持 `defer`,`additionalContext` 会被忽略,`updatedInput` 会被记录 + 警告但不应用(见 [pre-tool-input-rewrite Agent Note](../../../.agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.md))。
|
||||
- **`PostToolUse` 只支持部分功能:** 支持阻塞反馈与 JSON `additionalContext`,但不支持 `updatedToolOutput` 和 `updatedMCPToolOutput`,`tool_response` 会展平为文本。
|
||||
- **`SubagentStart` 与 `SubagentStop` 只支持部分功能:** 两者均报告常量 `agent_type`,其值为 `general-purpose`,并在 Claude Code 报告父会话的位置使用 child 会话 id。Start 上下文是尽力而为,且只能到达实时同进程 child;stop 只观测,无法阻塞 subagent 或向其提供上下文。Start 省略 `transcript_path`;stop 还省略 `agent_transcript_path`、`last_assistant_message`、`background_tasks` 和 `session_crons`,并始终报告 `stop_hook_active: false`。
|
||||
- **`SubagentStart` 与 `SubagentStop` 只支持部分功能:** 两者均报告常量 `agent_type`,其值为 `general-purpose`,并在 Claude Code 报告父会话的位置使用 child 会话 id。Start 上下文是尽力而为,且只能到达仍在运行的同进程 child;stop 只观测,无法阻塞 subagent 或向其提供上下文。Start 省略 `transcript_path`;stop 还省略 `agent_transcript_path`、`last_assistant_message`、`background_tasks` 和 `session_crons`,并始终报告 `stop_hook_active: false`。
|
||||
- **`Stop` 只支持部分功能:** 阻塞会强制另一个模型轮次,但 `stop_hook_active` 始终为 `false`,会省略 `last_assistant_message`、`background_tasks` 和 `session_crons`,且未实现连续阻塞上限(`TODO(stop-loop-guard)`)。因此,无条件阻塞 hook 会在每个步骤中强制 continuation,除非它自我限制。
|
||||
- **通用 payload 与输出字段只支持部分功能:** 已映射事件会省略 Claude Code 原本会提供的 `prompt_id`、`transcript_path`、`permission_mode` 和 `effort`。`systemMessage` 会被记录 + 警告但不呈现;`{"continue": false}` 会被记录但不会停止运行;不会应用 `suppressOutput`、`stopReason` 和 `terminalSequence`(`TODO(hook-continue-false)`)。
|
||||
- **Handler 与配置只支持部分功能:** 只运行 shell 形式 command handler。会跳过 `http`、`mcp_tool`、`prompt` 和 `agent` handler;不遵循 `args`、`async`、`asyncRewake`、`shell`、`if`、`once` 和 `statusMessage` 等 command handler 选项。匹配 handler 串行运行且不去重,而 Claude Code 会并行运行并对相同 handler 去重。一个进程级 `configPath` 会在加载时解析一次;尚未实现 Claude Code 的分层项目、user、plugin 与 policy 发现和实时重新加载(`TODO(per-session-hook-config)`)。
|
||||
- **Handler 与配置只支持部分功能:** 只运行 shell 形式 command handler。会跳过 `http`、`mcp_tool`、`prompt` 和 `agent` handler;不遵循 `args`、`async`、`asyncRewake`、`shell`、`if`、`once` 和 `statusMessage` 等 command handler 选项。匹配 handler 串行运行且不去重,而 Claude Code 会并行运行并对相同 handler 去重。一个进程级 `configPath` 会在加载时解析一次;尚未实现 Claude Code 的分层项目、用户、插件与策略发现和实时重新加载(`TODO(per-session-hook-config)`)。
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
一个 cordis 插件,在 harness 的规范拦截 seam 上运行 user 现有 **Codex** hook 配置的受支持子集。它是 hooks 子系统的 **Codex 方言** 一半。方言无关原语来自 [`@deepseek-ai/dsh-hook-protocol`](../hook-protocol/README.md);该桥接拥有 Codex 形状 payload、matcher 模式和决策映射。
|
||||
一个 Cordis 插件,在 harness 的规范拦截 seam 上运行用户现有 **Codex** hook 配置的受支持子集。它是 hooks 子系统中采用 **Codex 方言** 的一侧。方言无关原语来自 [`@deepseek-ai/dsh-hook-protocol`](../hook-protocol/README.md);该桥接负责处理 Codex 形状的 payload、matcher 模式和决策映射。
|
||||
|
||||
该桥接实现 Codex 当前 hook 协议的一个明确子集:
|
||||
|
||||
- **10 个 hook 点中的 5 个:** `PreToolUse`、`PostToolUse`、`SessionStart`、`UserPromptSubmit` 和 `Stop`。
|
||||
- **只使用正则 matcher**(没有字面快速路径;matcher 始终是未锚定正则)。
|
||||
- **snake_case stdin payload**,携带 `turn_id`/`model` 额外字段,写入时**不带** 尾随换行符。
|
||||
- **snake_case stdin payload**,携带 `turn_id`/`model` 额外字段,写入时**不带**尾随换行符。
|
||||
- **没有 Codex 插件 env 注入,也没有配置时 placeholder 替换**(命令仍会接收执行器环境,并通过其 shell 运行)。
|
||||
- **没有工具前批准或改写路径**:hook 可以阻塞,但桥接不会预批准或替换工具输入。
|
||||
- **没有工具前审批或改写路径**:hook 可以阻塞,但桥接不会预审批或替换工具输入。
|
||||
|
||||
原生 cordis 插件可以完成此桥接的所有工作,并且功能更强;该桥接只是已映射 Codex 子集的兼容路径(见 [拦截 seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-30-interception-seams.md))。
|
||||
原生 Cordis 插件可以完成此桥接的所有工作,并且功能更强;该桥接只是已映射 Codex 子集的兼容路径(见 [拦截 seam Agent Note(agent 决策记录)](../../../.agents/notes/implemented/feature/2026-06-30-interception-seams.md))。
|
||||
|
||||
## 配置
|
||||
|
||||
@@ -34,25 +34,25 @@ const config: Config = {
|
||||
model: deepseek-v4
|
||||
```
|
||||
|
||||
配置只在加载时解析**一次**。`configPath` 是**进程级** 配置:相对路径在加载时根据进程启动 cwd 解析,而非每会话解析(`TODO(per-session-hook-config)`)。读取/解析失败会被容纳(记录 + 不注册任何内容)。只运行同步 `type: 'command'` hook;非 command 或 `async: true` hook 会被解析并跳过,同时记录警告。hook 接受 `timeout` 或 `timeoutSec` alias;两者都未设置时,使用协议参考默认值 `DEFAULT_HOOK_TIMEOUT_MS`(来自 `dsh-hook-protocol`,10 分钟)。五个桥接支持点之外的事件会在解析时丢弃。
|
||||
配置只在加载时解析**一次**。`configPath` 是**进程级**配置:相对路径在加载时根据进程启动 cwd 解析,而非每会话解析(`TODO(per-session-hook-config)`)。读取/解析失败会被隔离处理(记录 + 不注册任何内容)。只运行同步 `type: 'command'` hook;非 command 或 `async: true` hook 会被解析并跳过,同时记录警告。hook 接受 `timeout` 或 `timeoutSec` alias;两者都未设置时,使用协议参考默认值 `DEFAULT_HOOK_TIMEOUT_MS`(来自 `dsh-hook-protocol`,10 分钟)。五个桥接支持点之外的事件会在解析时丢弃。
|
||||
|
||||
hook 本身会在 agent 的会话工作区中运行:对 agent scope 点,桥接会将会话 `cwd` 作为 hook 进程工作目录,因此 hook 作用于 user 项目树,而非服务器启动目录。
|
||||
hook 本身会在 agent(智能体)的会话工作区中运行:对 agent scope 点,桥接会将会话 `cwd` 作为 hook 进程工作目录,因此 hook 作用于 用户项目树,而非服务器启动目录。
|
||||
|
||||
## Hook 点 → seam Decision
|
||||
|
||||
| Codex hook | Harness seam | 映射 |
|
||||
|---|---|---|
|
||||
| `SessionStart` | `agent/session-start`(emit) | 纯 stdout hook 的输出 → additionalContext → `agent.inject()` |
|
||||
| `UserPromptSubmit` | `agent/prompt-submit`(waterfall) | `block`(退出码 2)→ `PromptDecision.block`;仅 additionalContext → 通过 `next()` 委托,再将一个单独标记源的上下文前置到下游 `additionalContexts` |
|
||||
| `UserPromptSubmit` | `agent/prompt-submit`(waterfall(瀑布式事件)) | `block`(退出码 2)→ `PromptDecision.block`;仅 additionalContext → 通过 `next()` 委托,再将一个单独标记源的上下文前置到下游 `additionalContexts` |
|
||||
| `PreToolUse` | `tools/pre-execute`(waterfall) | `block` → `PreToolDecision.deny`(没有 `allow`/`ask`) |
|
||||
| `PostToolUse` | `tools/post-execute`(waterfall) | `block` → 带反馈的 `block`;仅 additionalContext → 通过 `next()` 委托,再将一个单独标记源的上下文前置到下游决策;Code Mode 将子调用上下文延迟到外层 `run_code` 结果 |
|
||||
| `Stop` | `agent/turn-stopping`(serial) | 阻塞 Stop hook 通过 `steer()` 送入其原因,强制再执行一步 |
|
||||
|
||||
工具调用的 payload 携带真实 `tool_name`(matcher 测试的相同值)与 Codex `tool_input: { command }` 形状(存在 `command` arg 时使用该值,否则使用 `''`)。matcher subject 是工具名称(`PreToolUse`/`PostToolUse`)或会话源(`SessionStart`);`UserPromptSubmit`/`Stop` 忽略 matcher。
|
||||
|
||||
每个 agent scope stdin payload 都携带 `session_id` 和 `transcript_path`。可用时,桥接通过 `ctx.sessionPersistence.locate(session.header)` 解析后者,否则发送 `null`,保留 Codex `string | null` 形状。查找不会创建或 flush 产物,因此第一个轮次结束检查点之前路径可能不存在,也可能省略当前开启轮次。
|
||||
每个 agent scope stdin payload 都携带 `session_id` 和 `transcript_path`。可用时,桥接通过 `ctx.sessionPersistence.locate(session.header)` 解析后者,否则发送 `null`,保留 Codex `string | null` 形状。查找不会创建或 flush 产物,因此在第一个轮次结束检查点之前,路径可能尚不存在,或其指向的 transcript 可能尚未包含当前未结束的轮次。
|
||||
|
||||
`SessionStart` 是唯一的 emit 点,它会脱离运行。每条运行链都会被跟踪;dispose 桥接会中止仍在运行的 hook 进程,再排空 continuation,然后 dispose resolve(`createDetachedRuns`,位于 `dsh-hook-protocol`)。
|
||||
`SessionStart` 是唯一的 emit 点,它会脱离运行。每条运行链都会被跟踪;对桥接执行 dispose(资源释放)会中止仍在运行的 hook 进程,再排空 continuation,之后 dispose 才会完成(`createDetachedRuns`,位于 `dsh-hook-protocol`)。
|
||||
|
||||
## 上下文源
|
||||
|
||||
@@ -64,15 +64,15 @@ hook 本身会在 agent 的会话工作区中运行:对 agent scope 点,桥
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
`SessionStart`、已接受提示词和工具后 hook 可以添加带源归因的上下文消息;阻塞 `Stop` hook 将其原因添加为下一步 steering。
|
||||
`SessionStart`、已接受提示词和工具后 hook 可以添加带源归因的上下文消息;阻塞 `Stop` hook 将其原因添加为下一步 steering(中途引导)。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
hook 不返回上下文时没有成本。Hook 文本取决于数据,会被记录,并重发直到压缩。
|
||||
hook 不返回上下文时没有成本。Hook 文本取决于数据,会被记录,并重发直到压缩(compaction)。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
仅追加;新可见内容位于可复用请求前缀之后,不会使现有 KV-cache 配置项失效。
|
||||
仅追加;新可见内容位于可复用请求前缀之后,不会使现有 KV-cache 条目失效。
|
||||
|
||||
### 已阻塞提示词或工具结果
|
||||
|
||||
@@ -96,5 +96,5 @@ hook 不返回上下文时没有成本。Hook 文本取决于数据,会被记
|
||||
- **`PreToolUse` 只支持部分功能:** 支持阻塞,但会忽略 `additionalContext`、`permissionDecision: "allow"` 和 `updatedInput`。每个工具都表示为 `tool_input: { command }`,因此非 shell 工具参数不会如实公开给 hook。
|
||||
- **`PostToolUse` 只支持部分功能:** 支持阻塞反馈与 JSON `additionalContext`,但不会强制执行 `{"continue": false}`,非 shell 工具参数会缩减为 `{ command }`,结构化工具输出会在 `tool_response` 中展平为文本。
|
||||
- **`Stop` 只支持部分功能:** 阻塞会强制另一个模型轮次,但 `stop_hook_active` 始终为 `false`,`last_assistant_message` 始终为 `null`,且不会强制执行 `{"continue": false}`。因此,无条件阻塞 hook 会在每个步骤中强制 continuation,除非它自我限制(`TODO(stop-loop-guard)`)。
|
||||
- **通用 payload 与输出字段只支持部分功能:** 每个已映射事件都报告 `transcript_path: null`、静态配置的 `model` 与 `permission_mode: "default"`,而非当前 Codex 运行时值。`systemMessage` 会被记录 + 警告但不呈现,`{"continue": false}` 会被记录但不会应用 Codex 事件特定停止行为(`TODO(hook-continue-false)`)。
|
||||
- **配置加载与执行只支持部分功能:** 一个进程级 `configPath` 会在加载时解析;尚未实现 Codex 的活动 user、project、session、system/managed 和 plugin 分层、信任控制与内联 `config.toml` hook 形式(`TODO(per-session-hook-config)`)。只运行同步 `command` handler,忽略 `statusMessage` 与 `commandWindows` 等当前元数据,匹配 handler 串行运行,而非使用 Codex 的并发启动语义。
|
||||
- **通用 payload 与输出字段只支持部分功能:** 每个已映射事件都报告 `transcript_path: null`、静态配置的 `model` 与 `permission_mode: "default"`,而非当前 Codex 运行时值。`systemMessage` 会被记录并触发警告,但不呈现,`{"continue": false}` 会被记录但不会应用 Codex 事件特定停止行为(`TODO(hook-continue-false)`)。
|
||||
- **配置加载与执行只支持部分功能:** 一个进程级 `configPath` 会在加载时解析;尚未实现 Codex 的活动用户层、项目层、会话层、系统/托管层和插件层、信任控制与内联 `config.toml` hook 形式(`TODO(per-session-hook-config)`)。只运行同步 `command` handler,忽略 `statusMessage` 与 `commandWindows` 等当前元数据,匹配 handler 串行运行,而非使用 Codex 的并发启动语义。
|
||||
|
||||
Reference in New Issue
Block a user