# Agent Note: 持久的逐步骤时间上下文

Status: implemented

[English](2026-07-16-durable-per-step-time-context.md) | 中文

## 问题

仅存在于请求中的时钟可以告诉模型当前时间，但在系统提示词中替换这个值会移除先前对时间敏感的推理所依据的证据。在包含多个步骤的轮次中，请求需要保留先前步骤使用的读数。系统必须能在重启后重建请求，自动压缩（compaction）也必须将模型实际收到的同一份时间上下文纳入考量。

进程本地刷新缓存会使显示时间依赖于无法在恢复后保留的状态。来自浏览器的自然语言也需要归属于请求的时区：服务端进程时区无法推断用户所在地，而可变的会话或连接默认值会让旅行或并发标签页重新解释另一条提示词。

## 决策

`@deepseek-ai/dsh-time-context` 是位于 `packages/context/time-context/`、需要显式启用的函数插件。默认组合不启用其披露内容与 token 成本；Schedule Web overlay 会挂载它，使模型能够按附加到当前请求的浏览器时区解释未明确限定时区的日期和时间。

该插件会前置一个 `agent/pre-step` 监听器，并先行委托下游。当下游决策进入步骤且需要生成读数时，插件会把该决策的最终消息与开放轮次中已有的持久用户消息合并，从确切的 user-rpc 来源派生浏览器时区来源信息，并向该决策追加一条读数。决策被拒绝、监听器失败或信号已经中止时，不会记录任何内容。在当前批次之后被认领的 steering（中途引导）仍归属于普通的下一步骤，并在该步骤进入时获得新读数。

每条 Web 提示词都会采样浏览器的 IANA 时区。Host 校验并规范化该值，再将其绑定到确切的持久用户消息来源。开放轮次中唯一一个时区可解析请求；多个时区会产生排序后的 `mixed` 结果；没有时区则为 `unavailable`。解析成功的请求会告诉模型，把未限定时区的日期和时间解释为该时区。来源信息混杂或不可用时，模型会收到要求用户澄清的指令。

这种与消息绑定的来源信息不会复制到 `SessionHeader`、连接默认值或 Schedule 状态。Time-context 只负责模型指导。接受本地日历字段的工具仍必须自行定义显式边界；因此 Schedule 要求 `time_zone`，而不是导入该插件的读数（[决策](../simplification/2026-08-09-explicit-schedule-time-zone.md)）。

解析后的浏览器时区也用于格式化读数中的时间戳。请求来源信息混杂或不可用时，使用配置的 `timeZone` 回退值；如果省略该配置，则使用插件加载时解析一次的 Node 进程时区，同时仍保留要求澄清的策略。每个回退值都经 `Intl.DateTimeFormat` 校验。

每个读数都使用确切的快照来源 `{ kind: 'plugin', plugin: 'time-context', form: 'snapshot', sections: [{ name: 'time-context', text: <same text> }] }`。不变式配套模块会校验快照形状，从原始 user-rpc 消息重新派生当前轮次的浏览器来源信息，并校验渲染的时间戳时区与经过时长基线。

可选配置 `refreshIntervalMs` 必须是非负安全整数。省略或设为 `0` 时，每个符合条件且已进入的步骤都会注入。设为正数时，插件会扫描原始会话事件，查找最新的插件读数；不存在读数、挂钟时间倒退或事件已达到相应时长时执行注入。事件时间戳在压缩和恢复后仍是判断依据，无需进程本地缓存。Schedule Web overlay 会省略该间隔，使每个请求步骤都获得当前浏览器时区指导。

### 文本与时长基线

已解析的第一步读数为：

```text
Time sampled while preparing turn <turn>, step 1: <timestamp-in-browser-zone>
Browser time zone for this request: <iana-zone>. Interpret otherwise-unqualified dates and times in this zone.
Elapsed since the preceding model-visible message: <duration-or-unavailable>.
```

混杂和不可用的变体会把第二行替换为要求澄清的指令。基线是最新一条在其之前持久化的用户、助手或工具结果消息。为该步骤拟议的提示词尚未追加，因此新会话可能报告 `unavailable`。

后续步骤读数会改变第一行的步骤号，并以下行结束：

```text
Elapsed since the preceding step context: <duration-or-unavailable>.
```

其基线是开放轮次中的前一个 time-context 事件。缺少基线时报告 `unavailable`；时长采用紧凑的整秒单位，并在挂钟时间倒退时限制为零。

### 持久性与重建

已进入的步骤会在 `step/start` 之后、请求派生之前，先追加其返回消息，再追加时间读数。后续准备失败时，读数可能留在历史中，因为它记录的是步骤进入，而不是成功传输。每个读数都作为普通表层节点保留，直至压缩将其遮蔽。正数间隔可以让后续请求复用现有历史，而不添加新读数。

插件不向系统提示词组装或 `request/header` 贡献任何内容。请求重建会在每个 `step/start` 取得完整的持久表层前缀，因此历史请求可以还原模型看到的确切时间与浏览器策略。

## 已考虑的替代方案

- **替换动态系统提示词值**：不予采纳，因为替换会抹去先前读数，并改变重建后的历史请求。
- **持久化会话默认时区**：不予采纳，因为浏览器事实只属于一条提示词；旅行与并发标签页不得修改共享含义，也不得把时区状态扩散到会话、fork 与持久化约定中。
- **把浏览器时区复制到第二个上下文权威**：不予采纳，因为原始 user-rpc 来源已经拥有该值，不变式可以直接重新派生策略。
- **让 Schedule 隐式消费读数**：不予采纳，因为自然语言上下文不是稳定的类型化默认值，而且这会把绝对时间解析器耦合到 AgentLoop 历史。模型会改为传入显式偏移量或时区。
- **只使用进程时区**：不予采纳，因为部署所在地无法推断远程用户的时区。请求来源信息缺失或混杂时，它仍可作为显示回退值。
- **只通过工具提供时间**：不予采纳，因为普通时间推理会产生本可避免的往返，也无法确保每个步骤之前都有读数。
- **默认挂载 time-context**：不予采纳，因为披露内容、新鲜度与历史成本仍属于组合策略。

## 验证

单元测试和真实 agent loop（智能体循环）测试固定时间戳格式化、唯一／混杂／缺失浏览器时区的派生、回退显示、两种经过时长基线、间隔边界、跨轮次与恢复后的调度、挂钟倒退行为、steering 归属、取消、精确快照校验和请求重建。Host/client 测试固定浏览器采样，以及提示词进入时的校验与规范化。无密钥的组装 Schedule Web 场景发送一条真实浏览器提示词，在模型请求中观察到同一时区，并验证模型把该时区显式传给 `schedule_create`。

## 后果

- 浏览器时区含义归属于请求并可持久重建，无需更改会话、fork、JSONL 或 SQLite schema。
- 模型在每个 Schedule Web 请求步骤中都会收到所请求的浏览器本地假设；来源信息混杂或缺失时会询问，而不是猜测。
- 工具仍保持显式边界：上下文帮助模型选择字段，但不会成为包 seam 上隐藏的默认值。
- 时间上下文仅追加并保留到压缩为止；正数间隔会减少历史增长，但也可能使后续请求缺少新的浏览器时区指导。
