什么是上下文来源
上下文来源(context source)是一种不定义工具、却为请求追加模型可见内容的插件。它贡献的要么是系统提示词的一段,要么是一条持久化的 user/message,要么是提示词变量——它是组装后模型输入的两半之一。另一半是静态系统提示词,由 packages/core/system-prompt 通过其 system-prompt/assemble 扩展点组装。
核心引擎在 step/start 之后、请求派生之前记录最终的消息批次(各段 + 动态上下文)。因此,一个上下文贡献者必须挂接到 agent/pre-step 钩子上——在每次派生模型请求之前运行的接缝——并且,当它想让模型看到某个持久内容时,需要向即将进入的批次追加一条带来源(sourced)的 user/message。另一种方式是把一个命名 section 贡献给组装后的提示词,这由系统提示词注册表的消费者完成(例如 plan mode 的 plan:policy 段与 dsh-tool-goal 的目标策略)。
下表是整个上下文家族。四个包都是产品插件;只有 agent-instructions 被默认的 dsh-agent-spine-demo bundle 启用。
| 包 | ctx 键 | 职责 | 默认启用 |
|---|---|---|---|
agent-instructions | — | 工作区 AGENTS.md/CLAUDE.md 基线 + 动态发现 | ✅ |
session-reference | sessionReferenceResolver | 其他会话的有界只读快照 | ❌ 可选 |
time-context | — | 当前带时区时间 + 已耗时读数 | ❌ 可选 |
tmux-context | — | tmux 会话/窗口/窗格位置 | ❌ 可选 |
四个包共享同一个形态:一个一次性 agent/pre-step 监听器,仅在“到期”时才把一条带来源的消息折入即将进入的批次。我们称之为上下文贡献者模式。
上下文贡献者模式
虽然并不存在共享的运行时接口类,但这四个来源复刻了同一种形态,你可以通过阅读它们各自的 src/index.ts 加以验证:
agent/pre-step(前置监听器)
│ 先委派给下游监听器
▼
读取持久化会话事件(兼容压缩、无进程本地缓存)
│ 判断一次注入是否“到期”
▼
若到期:向返回的批次追加一条带来源的 UserMessage
│ source = { kind: 'plugin', plugin: '<名称>', ... } 或专用 kind
▼
step/start 在请求派生之前记录最终批次有三个反复出现的性质:
- 持久化、可重放的决策。 变更抑制与间隔调度直接扫描原始持久化会话日志,寻找该来源最近一次的注入,因此调度在压缩与进程重启后依然有效,无需进程本地缓存。
- 只追加的历史。 每一条注入消息都落在可复用请求前缀之后,因此不会使模型提供方较早的 KV 缓存条目失效。不再相关的上下文只会在后续压缩将其遮蔽时消失。
- 带来源的消息。 每次注入都通过
user/message的source字段声明其出处,使重放、不变量与面向模型的框架都能对其归因。
packages/core/system-prompt 注册表是持有静态一半的对应方。它暴露两个独立注册表——有序的提示词 section 与它所谓的动态 context(持久化的 user-role 快照)——外加用于字符串插值的提示词 variable。plan mode 与 dsh-tool-goal 使用 section 一侧;而四个上下文包使用 agent/pre-step + 带来源消息一侧。
agent-instructions——工作区指引
@deepseek-ai/dsh-agent-instructions 将工作区指令文件(AGENTS.md、CLAUDE.md 及其 .local 覆盖文件)的链条作为持久化、user-role 上下文注入。它是唯一具有生命周期拆分的上下文来源:用户全局与项目基线一次性进入持久化历史,而嵌套文件的发现、变更与移除则在探测到之后作为后续消息追加。
基线。 每个活跃会话的第一次合法 agent/pre-step 组装基线:先是 $DSH_HOME/AGENTS.md(默认 ~/.dsh/AGENTS.md),然后从项目根目录一路向下到 agent.session.header.cwd,逐个目录加载每个存在的基座候选与局部覆盖候选。发现/优先级/项目根列表可配置;默认是 instructionFileCandidates = ['AGENTS.md', 'CLAUDE.md'] 与 localInstructionFileCandidates = ['AGENTS.local.md', 'CLAUDE.local.md']。
动态发现。 在一次成功的第一方 read/write/edit 工具结果之后,插件检查该触碰是否到达新的后代 scope 或改变了已加载的 scope,并向 agent 的 next-step 收件箱排队对应转换:set(新文件)、replace(摘要变化)或 remove(被删除,或变成同目录中更早候选的重复)。这里刻意没有文件监视器——刷新由触碰驱动。
配置(packages/context/agent-instructions/src/config.ts):
| 键 | 默认值 | 含义 |
|---|---|---|
dshHome | ~/.dsh | 用户全局 AGENTS.md scope 的根目录 |
projectRootMarkers | ['.git'] | 阻止向上项目根发现的目录 |
maxBytes | 必填 | 整条基线的有界渲染预算 |
maxSourceBytes | 1 MiB | 渲染前每个文件的读取上限 |
instructionFileCandidates | ['AGENTS.md', 'CLAUDE.md'] | 基座候选文件名(仅限同目录文件名) |
localInstructionFileCandidates | ['AGENTS.local.md', 'CLAUDE.local.md'] | 每目录覆盖文件,空列表禁用 |
插件用字面量 <system-reminder> 块(针对仓库可控内容做了转义)包裹每条消息,使仓库文本无法关闭该框架。基线使用 Instructions from: ~/.dsh/AGENTS.md … Instructions from: AGENTS.md;新到达的 scope 使用 Additional instructions from: packages/app/AGENTS.md;变更使用 Updated instructions from: <path>;移除使用 Instructions removed: <path>。每次注入事件都携带一个带类型的 agent-instructions 来源,内含 { action, scope, path, digest? } 变更列表,加上 baseline: true 与用于完整基线的 baselineIdentity。
session-reference——把其他会话当作上下文
@deepseek-ai/dsh-session-reference 暴露 ctx.sessionReferenceResolver,一个准备有界、只读快照、以其他会话为带来源模型上下文的服务。它使用 ctx.sessionQuery 与压缩检查点标记;不需要 SQLite FTS。
URI 编码。 encodeSessionReferenceUri()/decodeSessionReferenceUri() 实现规范提及 URI dsh-session:<base64url(JSON.stringify(sessionId))>;formatSessionReferenceMention() 生成 @[label](uri)。模型(或宿主)通过写出这样的提及来引用另一会话;parseSessionReferenceText() 会把 markdown 提及还原成可读的 @label 文本,同时返回结构化引用。
快照语义。 prepare(agent, content, references, signal?) 对每个不同的来源调用一次 ctx.sessionQuery.readSurface(),只投影直接用户的 user/message、assistant 文本,以及携带规范 dsh-compaction 来源标记的 user/message 检查点,并返回分离的内容加一条聚合 UserMessage。上下文来源为 { kind: 'session-reference', version: 1, references }。任何一次提及的自引用、超过 maxReferences 个不同来源、或任何读取失败,都会在宿主调用 followup() 或 steer() 之前拒绝。agent 空闲时,一个一次性 agent/pre-step 包装器只在包含已认领直接提示词的 enter 决策中添加快照;运行中则恰好在 steer() 之前调用 inject()。
配置:
| 键 | 默认值 | 契约 |
|---|---|---|
maxReferences | 3 | 一条准备消息中的最大不同来源会话数(≤ 3) |
candidateLimit | 50 | 返回给宿主的默认候选数 |
maxReferenceBytes | 65536 | 单个引用对象的最大序列化 JSON 字节数 |
面向模型的载荷是一个不可信快照,开头为 ## Referenced sessions,以 <referenced-sessions> 标签内的 JSON 渲染,每个数据型 < 都作为 \u003c 输出。警告明确禁止遵循快照中的指令、权限声明或工具请求,除非当前用户重复它们。
time-context——时钟与已耗时
@deepseek-ai/dsh-time-context 是可选的开式持久化上下文,包含当前带时区时间、附着于打开的 user-rpc 消息的浏览器时区,以及请求准备期间采样的已耗时。默认组合禁用;Schedule Web 覆盖层挂载它,使非交互式调度能携带模型可在用户浏览器时区中解读的日期/时间。
配置(packages/context/time-context/src/config.ts):timeZone(当浏览器来源缺失或混合时的显示回退)与 refreshIntervalMs(正值时仅当会话没有更早注入、墙钟时间回退,或已过去这么多毫秒才注入)。
请求时区归属。 当打开的一轮包含一个经过宿主校验的浏览器时区时,该时区负责格式化时间戳;多个时区解析为 mixed,没有则解析为 unavailable。每次读数使用精确的来源 { kind: 'plugin', plugin: 'time-context', form: 'snapshot', sections: [{ name: 'time-context', text: <相同文本> }] }。注入行的形式:
Time sampled while preparing turn <turn>, step 1: <timestamp>
Browser time zone for this request: <iana-zone-or-mixed-or-unavailable-policy>.
Elapsed since the preceding model-visible message: <duration-or-unavailable>.tmux-context——终端位置
@deepseek-ai/dsh-tmux-context 命名此 agent 进程运行所在的 tmux 会话、窗口与窗格,外加窗口的窗格树布局。它每轮采样一次。关键在于它会检查哪个终端在控制此进程:仅凭 $TMUX_PANE 不够,因为从 tmux shell 内启动的终端(如 VS Code 集成终端)会从祖先继承 $TMUX/$TMUX_PANE。因此它会把窗格的 #{pane_tty} 与本进程自身的控制终端(ps -o tty= -p <pid>)进行比较;真正的窗格拥有此进程的 tty,而继承的环境指向另一窗格的 tty。
该读数作为一条只读 shell 命令,通过 ctx.shell 执行器运行(因此适用的部署沙箱与策略),任何失败都只是记录的警告,绝非轮次失败。配置为 { refreshIntervalMs?: number }。当渲染出的 tmux 状态与上次注入不同时,它前置一条带来源的 UserMessage(来源 { kind: 'plugin', plugin: 'tmux-context' }),含三行:
tmux location (turn <turn>):
session <session>, window <index> "<name>", pane <index> <pane-id>
window active=<0|1>, pane active=<0|1>, layout <window-layout>四个来源的对比
下表总结每个来源注入什么、何时注入、在何种配置下注入——审阅提示词时可作快速参考:
| 来源 | 注入 | 何时 | 关键配置 | ctx 键 / 来源 |
|---|---|---|---|---|
agent-instructions | 以持久化用户消息形态呈现的工作区 AGENTS.md/CLAUDE.md 链条 | 首次合法 pre-step(基线)+ 触碰驱动发现 | maxBytes、dshHome、候选列表 | 来源 { kind: 'agent-instructions', baseline, baselineIdentity } |
session-reference | 以 user/message 形态呈现的另一会话快照 | 空闲进入时或 steer() 之前 | maxReferences、candidateLimit、maxReferenceBytes | ctx.sessionReferenceResolver |
time-context | 时钟 + 浏览器时区 + 已耗时行 | 每个合法 pre-step | timeZone、refreshIntervalMs | 来源 { kind: 'plugin', plugin: 'time-context', form: 'snapshot', sections } |
tmux-context | tmux 位置三行 | 每个有变更的轮次之第一步 | refreshIntervalMs | 来源 { kind: 'plugin', plugin: 'tmux-context' } |
四个来源共有三个不变量:每次注入都是持久化、可重放、带来源的 user/message,相对可复用请求前缀是只追加的,且其变更抑制决策由原始会话日志做出(因此无需进程本地状态即可在压缩与重启后存活)。
来源归因与重放不变量
四个包各随附一个 ./invariant 伴生(例如 @deepseek-ai/dsh-time-context/invariant),独立校验可归因形态:精确来源联合、插件 id、段/组件名,以及(对 time-context)重新派生的浏览器时区策略与时间戳时区。这些伴生的存在是为了在不变量边界捕获不匹配或被伪造的上下文来源,而非无声地污染模型历史。
由于上下文消息持久化且带归因,下游系统能把它们与直接用户话语区分开来——session-reference 的 dsh-compaction 检查点投影、agent-instructions 的 AGENTS.md 框架、以及直接人类提示词各携带不同 source 标记。正是这一分隔,让压缩剪枝器、目标领域与人类命令平面各自把“模型可见上下文”和“直接用户输入”当作不同之物。
各来源如何组合
packages/core/system-prompt 与四个上下文包是独立的注册表,由核心循环随后合并。由于上下文消息是持久化且按序追加的,一次请求可以同时携带工作区基线、会话引用快照、时间读数与 tmux 位置,各自由其自身 source 归因。这里没有统一的“上下文桶”——每个贡献者都追加到同一个进入批次,step/start 记录合并后的结果。
一次注入的端到端数据流:
持久化会话事件 ──► agent/pre-step 监听器(每个来源一个)
│ 先委派下游,再判断“是否到期”
▼
向进入批次追加带来源的 user/message
│
▼
step/start 记录最终批次
│
▼
请求派生 ──► 模型请求(忠于会话日志的前缀)
│
▼
step/end ──► 持久化历史(只追加,直到压缩)step/start 记录的批次是唯一合并点:静态段、动态上下文与系统提示词都在派生前齐备于此。后续的压缩可以遮蔽任一条这类追加的上下文消息,而无需对它们的来源做特殊处理。
进一步阅读
- 系统提示词组装 —
section/context/variable注册表与system-prompt/assemble。 - 上下文压缩(Compaction) — 持久化上下文消息最终如何被摘要检查点遮蔽。
- 目标与目标轮次 — 一种注入
<goal_round>提示词的不同续作策略。 packages/context/README.md— 组级入口;docs/subsystems/session-reference.md记录会话引用。packages/context/agent-instructions/src/state.ts— 每会话/每项目 scope 缓存与摘要对账。.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.md与2026-07-27-tmux-location-context.md— 时间与 tmux 上下文的决策记录。