# Agent Note: 工作区上下文指令文件

Status: implemented

[English](2026-06-24-workspace-context.md) | 中文

## 问题

`AGENTS.md` 等仓库指引应当进入编码会话的有效上下文，使项目约定、构建命令和评审规则无需由用户反复粘贴即可生效。stdio 与 ACP（Agent Client Protocol）产品需要具备相同行为，并按会话 cwd 隔离：全局系统提示词章节会把一个工作区的文件泄漏到另一个仍在运行的 ACP 会话中。

相邻产品形成了值得借鉴的约定，但具体做法各不相同。Codex 原生使用 `AGENTS.md`；Claude Code 使用 `CLAUDE.md`，并采用熟悉的 system-reminder 风格用户上下文；opencode 同时支持这两个名称，每个目录只选一个胜出者，并延迟发现嵌套文件。harness 需要跨工具兼容，同时避免从同一作用域加载重复或互相矛盾的文件。

生命周期中有两类截然不同的内容。初始适用文件链在第一次请求前一次性注入。嵌套文件、编辑、候选项切换和移除发生在其后，进入同一份持久的仅追加历史。

## 决策

该实现在 `packages/context/agent-instructions` 中，包名为 `@deepseek-ai/dsh-agent-instructions`。它是请求上下文扩展，不是核心服务或文件系统后端。共享 demo 主干与 Host Runtime 根据显式的 `{ maxBytes } | false` 部署选择挂载它；`dsh web` 启用 65,536 字节预算，Host Runtime 的 headless 消费方则禁用它。该插件使用 `agent/pre-step`、不可变的 `tools/result` 结果、`session/event` 边界和可选的 `ctx.fs` 功能。

插件不会静态注入 `fs`。因此，不带提供方的产品树仍能正常启动；在文件系统提供方出现之前，插件保持无操作。所有生产读取都通过该提供方完成。候选项探测会解析每个路径并对结果执行 stat，因此会跟随最终路径组件的符号链接至其目标：指向普通文件的链接会被加载，缺失路径或非文件目标则确认为不存在。允许仓库拥有的链接跨越信任边界，是对最初不跟随探测方式的刻意反转；[跟随指令符号链接记录](2026-07-21-follow-instruction-symlinks.md)负责说明该决策及其残余风险。步骤信号与动态工具执行信号会贯穿解析、元数据探测和流式读取，因此取消不会等待无关的文件系统扫描。解析或 stat 异常归类为不可用：它只跳过该候选项，绝不被解释为已经加载的作用域被删除。

### 文件名与优先级

默认的逐目录候选列表是 `['AGENTS.md', 'CLAUDE.md']`。该列表可通过 `instructionFileCandidates` 配置；`AGENTS.md` 是普通的第一候选项，而不是隐藏优先级。一个目录中只加载第一个存在的普通文件候选项。使用默认值时，`AGENTS.md` 是原生文件，`CLAUDE.md` 是兼容性回退。第二个列表 `localInstructionFileCandidates`（默认为 `['AGENTS.local.md', 'CLAUDE.local.md']`）会在同一目录的基础文件后加载叠加式本地覆盖层；[默认本地覆盖层记录](2026-07-21-local-instruction-overlay.md)负责说明该决策。

候选条目必须是同一目录中的文件名。空条目、`.`／`..`，以及包含 `/` 或 `\` 的条目会被忽略。其他同目录名称可以显式选择加入；规则目录和导入语义不属于本约定。

用户全局文件固定为 `$DSH_HOME/AGENTS.md`，不受任一候选列表影响，也没有本地覆盖层。`$DSH_HOME` 默认为 `~/.dsh`，与 `~/.codex` 或 `~/.claude` 在 harness 层的 home 角色一致，而不会引入插件专用 home。波浪号展开与默认值位于 `dsh-home-paths` 中，以便未来的 harness 功能共享同一约定。

### 基线注入

在 agent loop（智能体循环）实例的第一个 `agent/pre-step`，插件会组合一条带来源的 user 角色基线。当下游决策让非空的第一步批次进入时，插件会将基线折入最终批次、紧随已领取的直接提示词之后，使其与直接提示词一同成为持久记录并抵达第一次请求。reject 或空的第一步决策会将基线留在 next-step inbox，等待后续唤醒。插件先加载用户全局文件，再从 `agent.session.header.cwd` 向上遍历至配置的根标记（默认为 `.git`）以确定项目根目录，随后从根目录至 cwd 的每级目录加载已配置候选项。`.git` 文件与 `.git` 目录都是有效标记，因而能覆盖链接 worktree 和 submodule。找不到标记时，cwd 本身就是根目录。

该基线会成为一条持久 `user/message`，并携带带类型的 `agent-instructions` 来源。其 `baseline: true` 标记将完整基线与后续增量区分开来，`baselineIdentity` 记录规范化的发现、优先级、项目根目录和预算语义，变更列表则持久保存已纳入的作用域和内容 digest。若先前排队的 workspace 基线仍在等待，插件会删除该确切消息并 prepend 替代值，而不会累积副本。

恢复 agent 会基于持久化历史创建新的 loop 实例。在第一个 `agent/pre-step`，具有当前标识的可见基线仍是权威状态；插件会将其保留的 scope 与当前完整渲染进行比较。未变化和被预算省略的文件不追加任何内容；agent 离线期间新增、编辑、移除或不再属于预算保留集的文件，会在进入步骤的批次中追加 `set`、`replace` 或 `remove` 转换，既不改写也不重复原始基线。不兼容的可见基线会被一条按当前优先级排列的完整基线取代，并以明确措辞说明替换关系；如果当前不存在任何候选文件，一条显式空基线会清除先前的 scope。插件热重挂遵循相同规则。如果压缩（compaction）已遮蔽带类型的基线，下一次进入步骤的 pre-step 会组合一条完整的当前基线，并在同一请求中携带它。

基线是一条 user 角色的 `<system-reminder>`，包含 `Instructions from: <path>` 章节，以及明确的权威性与优先级说明。这种熟悉的模型可见框架避免引入 harness 专用的 XML 词汇。项目路径相对于根目录；使用默认 home 时，用户全局路径为 `~/.dsh/AGENTS.md`，使用已配置 home 时则为 `$DSH_HOME/AGENTS.md`。最终渲染边界会在完成字节核算前，转义指令内容或模型可见的路径、scope 与预算元数据中出现的字面量 `</system-reminder>`。包 README 负责规定当前准确的[提示词形态](../../../../packages/context/agent-instructions/README.md#prompt-shape)。

### 动态发现与刷新

第一方 `read`、`write` 或 `edit` 调用成功后，不可变的 `tools/result` 观察器会协调被触及的后代路径链，以及该会话已经知道的每个作用域，然后在 agent inbox 中排入一条 `Additional instructions from: <path>` system-reminder，供下一次请求使用。在 Code Mode 下，成功的子分派 touch 会沿不透明的父级执行 token 逐层上浮，直到顶层结果落定。在 agent loop 步骤内产生的 touch，须等持久 `step/end` 后才开始异步投影；打开的步骤之外直接执行工具时，则立即投影。这两个边界在不让工具流水线等待文件系统发现的前提下，保证结果／步骤的相邻关系具有确定性。

内容编辑会追加 `Updated instructions from: <path>`，说明新内容取代先前内容，并包含当前的完整文件。如果优先级从一个候选项变为另一个，消息还会指出先前路径并说明它不再适用。如果没有候选项保留，插件会追加 `Instructions removed: <path>`，并说明先前加载的指令不再适用。

基线消息和动态消息都在 `content` 中携带完整的 system-reminder 框架；每条带来源的 `user/message` 都逐字抵达模型，核心层不会再添加可选择退出的包装。带类型的 `agent-instructions` 来源携带持久化状态，该状态绝不会渲染给模型。

shell 命令不会触发发现。本地 bash 调用会启动全新的 shell，而从任意命令字符串推断已到达路径，需要实现提示词插件并不拥有的 shell 语义。

### 重复抑制与变更检测

每个工作区上下文事件都会存储带版本的元数据，其形态为 `{ action, scope, path, digest? }`；`digest` 是对已加载内容计算的 SHA-1。完整基线还会额外携带 `baseline: true` 和 `baselineIdentity`。模型可见提示词中没有 HTML 注释、隐藏标记，也没有会被解析回状态的标题。

协调时，插件扫描带工作区来源的 `user/message` 事件，并派生每个可见作用域的最新状态。即使后续复合结果被拦截，成功的嵌套 touch 也会聚合到父级执行 token 下；顶层结果会将它们交给打开的会话步骤，或直接交给逐 agent 投影队列。`step/end` 只会在自身边界持久化后释放其暂存的 touch；下一次 `agent/pre-step` 会等待串行投影完成。每次投影都会根据可见历史和当前 inbox 进行组合，并替换唯一一条待处理工作区上下文，而不会累积中间渲染结果。

路径和 digest 均未变化时会被抑制。日志中的移除操作是一条墓碑记录，因此重新出现的候选项会成为新的 `set`。恢复操作从持久化元数据继续工作：兼容的可见基线会提供比较状态，而不会导致再次追加完整基线。如果压缩从可见表面移除某条指令事件，该状态不再抑制后续加载，这与模型已经无法看见它的事实一致。变更只有在对应文件专属段落保留至少一个内容字节，或原始内容确实为空时，才会进入元数据或待处理状态。只要任一内容字节保留下来，部分截断就会提交完整内容 digest；截断到零内容仍可在后续触碰中处理。基线可以保留字节预算诊断而不提交任何变更。动态批次若没有可提交变更，则整批不注入，并在后续触碰中重试。被省略的文件仍可在之后的触碰中加载。

只有当初始基线事件仍在可见会话表层中时，其类型化变更才用作比较状态。恢复会保留兼容的基线，并对账当前基线和可见的动态 scope，因此 agent 离线期间的变更会在第一次恢复请求前追加为转换。当压缩遮蔽基线事件时，下一次进入步骤的 pre-step 会组合完整的当前基线，并在同一请求中记录它；也可以改由一次成功的文件系统触碰重新添加未变化的基线 scope，或把基线编辑或移除操作追加为动态消息。两条路径都不会重写原始事件。内存中的 scope 标记和提供方版本 cache 只用于选择探测对象并加速探测，因此二者都不能抑制模型已无法看见的上下文。

系统刻意不使用文件监视器。检测发生在下一次成功的结构化文件系统触碰、恢复时的基线对账，或进入步骤的 pre-step 恢复被遮蔽的基线时。提供方失败不会产生移除；只有该作用域中的全部已配置候选项都成功完成探测后，系统才接受「不存在」这一结论。

### 字节预算与有界读取

`maxBytes` 是必填项，分别作用于渲染后的基线或单个动态协调批次；系统不存在隐式或无界的渲染预算。非正数或非有限值会禁用加载。内容超过预算时，系统会先省略较宽泛的文件，再截断最具体的文件。可见的 `Workspace instruction budget ...` 提示会指出被省略和截断的路径与字节数，并且输出绝不超过配置字节数。

`maxSourceBytes` 是正数的逐文件上限，默认为 1 MiB。loader 会在读取前检查报告的大小，同时仍通过 `streamText()` 消费内容并持续统计 UTF-8 字节数，因此缺失／陈旧的元数据无法迫使其进行无界分配。过大的胜出候选项会被视为不可用，而不是改为同目录中的下一个名称。插件刻意不保留进程级缓存，也绝不保留指令正文。它只为每个有效作用域保存 `{ path, version, digest }`，并将这些状态放在 `WeakMap<Session, Map<scope, state>>` 中：提供方 `FsVersion` 与有效提示词状态同时匹配时跳过读取；版本变化则触发有界读取和 SHA-1 确认。SHA-1 仍是持久化在可见结构化元数据中的跨提供方内容标识；提供方版本只作为内存中的失效快速路径。动态缓存转换发生在串行投影组合唯一一条目标 inbox 上下文时；下一次 pre-step 会等待该投影完成，再决定哪些内容进入请求。

## 考虑过的替代方案

**使用全局 `ctx.systemPrompt.section()`。** 不予采纳，因为同一个 Cordis 上下文可以承载 cwd 不同的多个会话，而仓库自身拥有的文本属于低权威用户上下文，不是最高权威的提供方系统内容。

**始终把准备好的 workspace 上下文留在 inbox 中。** 不予采纳，因为在 pre-step 中准备的上下文会因此在当前请求结束后继续留存，并自行启动第二个模型步骤。inbox 仍作为暂存区以及 reject 时的后备，而进入步骤的 pre-step 负责随最终批次原子投递。

**在一个目录中同时加载 `AGENTS.md` 和 `CLAUDE.md`。** 不予采纳，因为正在迁移的仓库通常会在两个文件中重复指引。按顺序排列的候选项让优先级显式且可配置。

**解析渲染后的标题或隐藏注释以恢复已加载状态。** 不予采纳，因为指令正文可能包含相同文本，导致无提示的误报。持久化 JSON 元数据提供明确且对模型不可见的状态通道。

**使用模型总结文件。** 不予采纳，因为指令文件本身已经是经过整理的摘要；再执行一次模型调用既不确定，也可能抹掉边界情况要求。使用带字节预算的确定性全文更简单。

## 后果

工作区指引按会话隔离，并由 demo 入口、Web Host 与每一种工具展示模式共享。初始、嵌套与变更指令都保持持久且可回放。通用的会话／agent 上下文约定通过先在 inbox 中暂存、再持久进入的 user 消息携带带类型的来源数据，而不会把条目展平。

仓库文本仍是不受信任的输入。低权威 user 角色框架、显式优先级说明和分隔符转义可以降低风险，但无法消除提示词注入。跟随候选符号链接到目标，会把该攻击面扩大至树外内容；因此，把 `ctx.fs` 限制在可信根目录内的权限与沙箱层才是真正的边界，它们让系统把工作区文件当作数据而不是权威（[跟随指令符号链接记录](2026-07-21-follow-instruction-symlinks.md)负责说明残余风险）。

系统由事件驱动，而不是文件监视器驱动。除非文件系统变更通过结构化工具完成，否则编辑不会在确切的文件系统变更时刻可见；外部文件变更会在下一次成功的结构化触碰、恢复对账，或恢复被遮蔽的基线时被发现。这使设计保持确定性并且与提供方无关。

## 延后事项

从 bash 派生路径报告、递归启动扫描、文件监视器、小写默认名称、`.claude/CLAUDE.md`、`.claude/rules/*.md`、导入指令、ACP `additionalDirectories`、信任确认和模型生成摘要均延后处理。项目目录中的 `.local.` 覆盖层现已默认加载（[默认本地覆盖层记录](2026-07-21-local-instruction-overlay.md)负责说明该决策）；用户全局覆盖层、目录规则系统和导入仍需要各自的优先级与信任设计。
