# Conflicts: # .agents/notes/implemented/feature/2026-06-24-workspace-context.i18n.yaml # .agents/notes/implemented/feature/2026-06-24-workspace-context.md # .agents/notes/implemented/feature/2026-06-24-workspace-context.zh.md # docs/architecture.i18n.yaml # docs/architecture.md # docs/architecture.zh.md # docs/cordis-catalog/events.md # docs/core-data-structures/system-prompt.i18n.yaml # docs/event-producer-consumer.md # examples/acp-agent/tests/snapshots/workspace-context/session.jsonl # packages/context/workspace-context/README.i18n.yaml # packages/context/workspace-context/README.md # packages/context/workspace-context/README.zh.md # packages/context/workspace-context/src/index.ts # packages/context/workspace-context/tests/workspace-context.spec.ts # packages/core/agent-loop/README.i18n.yaml # packages/core/agent-loop/src/agent.ts # packages/core/agent/README.i18n.yaml # packages/core/agent/README.md # packages/core/agent/README.zh.md # packages/core/agent/tests/agent.spec.ts # packages/core/system-prompt/README.i18n.yaml
15 KiB
Agent Note: 工作区上下文指令文件
Status: implemented
English | 中文
问题
AGENTS.md 等仓库指引应当进入编码会话的有效上下文,使项目约定、构建命令和评审规则无需由用户反复粘贴即可生效。stdio 与 ACP(Agent Client Protocol)产品需要具备相同行为,并按会话 cwd 隔离:全局系统提示词章节会把一个工作区的文件泄漏到另一个仍在运行的 ACP 会话中。
相邻产品形成了值得借鉴的约定,但具体做法各不相同。Codex 原生使用 AGENTS.md;Claude Code 使用 CLAUDE.md,并采用熟悉的 system-reminder 风格用户上下文;opencode 同时支持这两个名称,每个目录只选一个胜出者,并延迟发现嵌套文件。harness 需要跨工具兼容,同时避免从同一作用域加载重复或互相矛盾的文件。
生命周期中有两类截然不同的内容。初始适用文件链在第一次请求前一次性注入。嵌套文件、编辑、候选项切换和移除发生在其后,进入同一份持久的仅追加历史。
决策
该实现在 packages/context/workspace-context 中,包(package)名为 @deepseek-ai/dsh-workspace-context。它是请求上下文扩展,不是核心服务或文件系统后端。共享 demo 主干与 Host Runtime 根据显式的 { maxBytes } | false 部署选择挂载它;dsh web 启用 65,536 字节预算,Host Runtime 的 headless 消费方则禁用它。该插件使用 agent/pre-step、tools/post-execute 和可选的 ctx.fs 功能。
插件不会静态注入 fs。因此,不带提供方的产品树仍能正常启动;在文件系统提供方出现之前,插件保持无操作。所有生产读取都通过该提供方完成。候选项探测会解析每个路径并对结果执行 stat,因此会跟随最终路径组件的符号链接至其目标:指向普通文件的链接会被加载,缺失路径或非文件目标则确认为不存在。允许仓库拥有的链接跨越信任边界,是对最初不跟随探测方式的刻意反转;跟随指令符号链接记录负责说明该决策及其残余风险。步骤信号与动态工具执行信号会贯穿解析、元数据探测和流式读取,因此取消不会等待无关的文件系统扫描。解析或 stat 异常归类为不可用:它只跳过该候选项,绝不被解释为已经加载的作用域被删除。
文件名与优先级
默认的逐目录候选列表是 ['AGENTS.md', 'CLAUDE.md']。该列表可通过 instructionFileCandidates 配置;AGENTS.md 是普通的第一候选项,而不是隐藏优先级。一个目录中只加载第一个存在的普通文件候选项。使用默认值时,AGENTS.md 是原生文件,CLAUDE.md 是兼容性回退。第二个列表 localInstructionFileCandidates(默认为 ['AGENTS.local.md', 'CLAUDE.local.md'])会在同一目录的基础文件后加载叠加式本地覆盖层;默认本地覆盖层记录负责说明该决策。
候选条目必须是同一目录中的文件名。空条目、./..,以及包含 / 或 \ 的条目会被忽略。其他同目录名称可以显式选择加入;规则目录和导入语义不属于本契约。
用户全局文件固定为 $DSH_HOME/AGENTS.md,不受任一候选列表影响,也没有本地覆盖层。$DSH_HOME 默认为 ~/.dsh,与 ~/.codex 或 ~/.claude 在 harness 层的 home 角色一致,而不会引入插件专用 home。波浪号展开与默认值位于 dsh-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,并携带带类型的 workspace-instructions 来源。其 baseline: true 标记将完整的启动或恢复基线与后续增量区分开来,变更列表则持久保存已纳入的作用域和内容 digest。若先前排队的 workspace 基线仍在等待,插件会删除该确切消息并 prepend 替代值,而不会累积副本。
恢复 agent 会创建新的循环实例,并在其第一次请求前注入由当前文件组合的基线。这样,恢复时可以使用当前基线内容,而无需修改先前的历史事件。恢复与插件热重挂都会面对日志中可能已存在基线的情况;二者通过 agent/session-start 区分:启动或恢复会在第一步前发出该事件,而热重挂附着到一个已存活的会话、永远不会看到它。只有当基线的类型化事件仍在当前可见表层中时,热重挂才保留既有基线,同时仍会根据当前文件重建 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 负责规定当前准确的提示词形态。
动态发现与刷新
第一方 read、write 或 edit 调用成功后,tools/post-execute 监听器会协调被触及的后代路径链,以及该会话已经知道的每个作用域。新到达的作用域通过 additionalContexts 返回,并在下一次请求中使用 Additional instructions from: <path> system-reminder。在 Code Mode 下,run_code 会把子分发上下文延后至其外层结果,因此同一更新只会在父结果之后追加,而不会在调用中途注入。
内容编辑会追加 Updated instructions from: <path>,说明新内容取代先前内容,并包含当前的完整文件。如果优先级从一个候选项变为另一个,消息还会指出先前路径并说明它不再适用。如果没有候选项保留,插件会追加 Instructions removed: <path>,并说明先前加载的指令不再适用。
基线消息和动态消息都在 content 中携带完整的 system-reminder 框架;每条带来源的 user/message 都逐字抵达模型,核心层不会再添加可选择退出的包装。带类型的 workspace-instructions 来源携带持久化状态,该状态绝不会渲染给模型。
shell 命令不会触发发现。本地 bash 调用会启动全新的 shell,而从任意命令字符串推断已到达路径,需要实现提示词插件并不拥有的 shell 语义。
重复抑制与变更检测
每个工作区上下文事件都会存储带版本的元数据,其形态为 { action, scope, path, digest? };digest 是对已加载内容计算的 SHA-1。基线还会额外携带 baseline: true。模型可见提示词中没有 HTML 注释、隐藏标记,也没有会被解析回状态的标题。
协调时,插件扫描带工作区来源的 user/message 事件,并派生每个可见作用域的最新状态。一个简短的逐会话待处理映射只会在不可变的顶层 tools/result 证明某个 additionalContexts 条目经过所有 post-execute 监听器后仍然保留时开始记录;随后,它覆盖循环将该上下文追加到日志之前的间隔。每个条目记录开启状态的 { turn, step }:如果相同的持久 user/message 出现在其序列边界或之后,该条目得到确认并被移除;如果匹配的 step/end 先到达,则说明循环丢弃了上下文缓冲区,插件会同时移除待处理条目及其版本缓存快速路径。嵌套的 Code Mode 结果会把变更暂存在父级的不透明执行 token 下,确保一次运行中的重复子分发不会产生重复项;父级结果会回滚这份临时状态,并且只提交外层策略保留的上下文。
路径和 digest 均未变化时会被抑制。日志中的移除操作是一条墓碑记录,因此重新出现的候选项会成为新的 set。恢复操作从持久化元数据继续工作。如果压缩从可见表面移除某条指令事件,该状态不再抑制后续加载,这与模型已经无法看见它的事实一致。只有真正纳入字节预算的变更才会进入元数据或待处理状态,因此被省略的文件在之后的触碰中仍有资格加载。
只有当初始基线事件仍在可见会话表层中时,其类型化变更才用作比较状态。当压缩遮蔽该事件时,下一次进入步骤的 pre-step 会组合当前基线,并在同一请求中记录它;也可以改由一次成功的文件系统触碰重新添加未变化的基线 scope,或把基线编辑或移除操作追加为动态消息。两条路径都不会重写原始事件。内存中的 scope 标记和提供方版本 cache 只用于选择探测对象并加速探测,因此二者都不能抑制模型已无法看见的上下文。恢复时准备基线的过程中,插件还会协调可见的动态作用域,因此 agent 离线期间发生的嵌套变更可以在第一次恢复请求前追加更新。
系统刻意不使用文件监视器。检测发生在下一次成功的结构化文件系统触碰或恢复时的基线准备。提供方失败不会产生移除;只有该作用域中的全部已配置候选项都成功完成探测后,系统才接受「不存在」这一结论。
字节预算与有界读取
maxBytes 是必填项,分别作用于渲染后的基线或单个动态协调批次;系统不存在隐式或无界的渲染预算。非正数或非有限值会禁用加载。内容超过预算时,系统会先省略较宽泛的文件,再截断最具体的文件。可见的 Workspace instruction budget ... 提示会指出被省略和截断的路径与字节数,并且输出绝不超过配置字节数。
maxSourceBytes 是正数的逐文件上限,默认为 1 MiB。loader 会在读取前检查报告的大小,同时仍通过 streamText() 消费内容并持续统计 UTF-8 字节数,因此缺失/陈旧的元数据无法迫使其进行无界分配。过大的胜出候选项会被视为不可用,而不是改为同目录中的下一个名称。插件刻意不保留进程级缓存,也绝不保留指令正文。它只为每个有效作用域保存 { path, version, digest },并将这些状态放在 WeakMap<Session, Map<scope, state>> 中:提供方 FsVersion 与有效提示词状态同时匹配时跳过读取;版本变化则触发有界读取和 SHA-1 确认。SHA-1 仍是持久化在可见结构化元数据中的跨提供方内容标识;提供方版本只作为内存中的失效快速路径。模型可见变更的缓存转换只有在相应上下文通过完整的工具结果策略链后才会提交;如果该已接受上下文随后与中止步骤一起被丢弃、未能进入日志,缓存转换就会失效。
考虑过的替代方案
使用全局 ctx.systemPrompt.section()。 不予采纳,因为同一个 Cordis 上下文可以承载 cwd 不同的多个会话,而仓库自身拥有的文本属于低权威用户上下文,不是最高权威的提供方系统内容。
始终把准备好的 workspace 上下文留在 inbox 中。 不予采纳,因为在 pre-step 中准备的上下文会因此在当前请求结束后继续留存,并自行启动第二个模型步骤。inbox 仍作为暂存区以及 reject 时的后备,而进入步骤的 pre-step 负责随最终批次原子投递。
在一个目录中同时加载 AGENTS.md 和 CLAUDE.md。 不予采纳,因为正在迁移的仓库通常会在两个文件中重复指引。按顺序排列的候选项让优先级显式且可配置。
解析渲染后的标题或隐藏注释以恢复已加载状态。 不予采纳,因为指令正文可能包含相同文本,导致无提示的误报。持久化 JSON 元数据提供明确且对模型不可见的状态通道。
使用模型总结文件。 不予采纳,因为指令文件本身已经是经过整理的摘要;再执行一次模型调用既不确定,也可能抹掉边界情况要求。使用带字节预算的确定性全文更简单。
后果
工作区指引按会话隔离,并由 demo 入口、Web Host 与每一种工具展示模式共享。初始、嵌套与变更指令都保持持久且可回放。通用的会话/agent 上下文契约通过注入消息与工具执行后的 additionalContexts 数组携带带类型的来源数据,而不会把条目展平。
仓库文本仍是不受信任的输入。低权威 user 角色框架、显式优先级说明和分隔符转义可以降低风险,但无法消除提示词注入。跟随候选符号链接到目标,会把该攻击面扩大至树外内容;因此,把 ctx.fs 限制在可信根目录内的权限与沙箱层才是真正的边界,它们让系统把工作区文件当作数据而不是权威(跟随指令符号链接记录负责说明残余风险)。
系统由事件驱动,而不是文件监视器驱动。除非文件系统变更通过结构化工具完成,否则编辑不会在确切的文件系统变更时刻可见;外部文件变更会在下一次成功的结构化触碰或恢复时被发现。这使设计保持确定性并且与提供方无关。
延后事项
从 bash 派生路径报告、递归启动扫描、文件监视器、小写默认名称、.claude/CLAUDE.md、.claude/rules/*.md、导入指令、ACP additionalDirectories、信任确认和模型生成摘要均延后处理。项目目录中的 .local. 覆盖层现已默认加载(默认本地覆盖层记录负责说明该决策);用户全局覆盖层、目录规则系统和导入仍需要各自的优先级与信任设计。