# Agent Note：可选时间上下文插件

Status: implemented

[English](2026-07-14-time-context-plugin.md) | 中文

## 问题

本记录中的动态系统提示词存储和刷新决策已由[持久的逐步骤时间上下文](2026-07-16-durable-per-step-time-context.md)取代。需要显式启用的包（package）、分区时间格式和校验仍然保留；后续 Agent Note 负责当前的模型可见与持久性契约。

如果部署方既未在提示词中提供时钟，也未给模型提供查询工具，agent（智能体）请求就无法获得实时准确的时间。静态文本会变得陈旧，而对于日期、截止时间或闲置时长等常规推理，调用工具会增加开销。缺少已经过去的时长时，模型无法区分紧接着发送的消息与上一条消息几小时后才发送的消息。

提示词组装流程可以在每个步骤中根据持久会话时间戳派生这两项信息，请求头日志则可以记录实际渲染的确切值。在会话历史中累积陈旧读数或唤醒空闲 agent 都会违反现有请求生命周期。

## 决策

`@deepseek-ai/dsh-time-context` 是位于 `packages/context/time-context/`、需要显式启用的函数插件。`context/` 产品分组用于容纳既不定义工具、也不定义服务的有界请求上下文增强。`dsh-agent-spine-demo` 和仓库提供的示例都不会加载该包；只有当 token 与信息披露成本可接受时，部署方才显式挂载它。

该插件注册顺序值为 10 的全局系统提示词区段 `context:time`，位置在部署方角色设定之后、工具指导之前。对于活跃轮次，它会输出带数字 UTC 偏移和 IANA 时区、形似 ISO 的时间戳，以及从轮次开始前最后一条模型可见消息起算的紧凑整秒时长。未绑定 agent 或 agent 处于空闲状态时，该区段为空。

### 上一条消息基线

在轮次首次组装时，提供方会在 `turn/start` 之前查找最近的 `user/message`、`assistant/message`、`tool/result`、`context/message` 或 `steering/message`。它会排除当前提示词，使时长表达轮次间隔，而不是接近零。同一轮次中的每次刷新都保留这条基线；首个轮次报告 `unavailable (no earlier message in this session)`。

基线采用会话事件的追加时间，而不是日志中不存在的客户端时间戳。因此，恢复和 fork 行为可以从持久日志中确定性重现，模型可见值也无需新增事件即可重建。系统挂钟向后调整时，插件会将时长钳制为零。

### 刷新策略

`refreshIntervalMs` 默认值为 60,000，并且必须是非负安全整数。每个轮次的首次请求都会刷新。同一轮次中的后续组装会复用该区块，直至其存在时间达到该间隔；设为 `0` 时每个步骤都刷新。刷新仅由请求驱动，因此在模型调用、工具运行或空闲期间，计时器不会创建任务。

省略 `timeZone` 时，`Intl.DateTimeFormat` 会在插件加载时解析一次 Node 进程的系统时区。Node 会遵循 `TZ`；没有该覆盖值时，时区由主机或容器提供。显式值必须是 IANA 标识符，并在加载时接受校验。捕获的时区在插件重新加载前保持稳定，形似 ISO 的本地时间戳包含其当前数字偏移，使夏令时变化保持显式可见。该默认值代表部署进程的时区，而不是远程用户的时区。

### 日志与 token 形态

agent loop（智能体循环）会在发送前通过完整的 `request/header` 快照记录时间区块，从而满足[可重建请求契约](../architecture/2026-07-05-reconstructable-requests.md)。每个请求只携带一个当前区块；先前的读数不会保留在会话历史中。该插件拥有时间信息，并按照[提示词变量 Agent Note](../architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md)通过提示词注册表贡献该信息，无需为循环添加特殊分支。

## 测试

单元测试固定格式化、基线、刷新策略、校验、逐 agent 状态、资源释放行为，以及系统时区在加载时的捕获行为。使用真实 agent loop 的测试固定实际发送的提示词和完整的 `request/header` 快照。无密钥子进程端到端测试通过真实 Loader 和 stdio 应用启动测试专用 `cordis.yml`，在受控 `TZ` 下省略 `timeZone`，驱动两个轮次，并从外部校验持久请求头。默认快照组合不包含该插件，因此其中的 transcript（文本记录）fixture（测试前置数据）不包含时间区块。

## 考虑过的替代方案

- **每个轮次或每次刷新都追加一条 `context/message`**——不予采纳，因为读数和 token 成本会在历史中累积。替换先前的表层节点会保留其旧位置，而替换尾部节点会隐藏中间的会话内容。
- **使用 `agent/session-prefix`**——不予采纳，因为会话期间保持稳定的前缀无法表示逐轮次或逐步骤变化的时钟。
- **在 `agent/request` 中修改请求**——不予采纳，因为该边界在消息边界之后塑造调用配置；插入模型可见内容会绕过提示词压力核算和请求头日志。
- **注册独立的 `{{current_time}}` 和 `{{elapsed}}` 变量**——不予采纳，因为独立提供方可能在不同时间点采样，并且需要共享缓存。单个区段会以原子方式记录两项信息，也不需要部署方编写时间模板。
- **通过后台计时器刷新**——不予采纳，因为请求组装之外没有消费新值的对象。由计时器驱动 `agent.inject()` 会创建轮次，并且只为报告时间流逝就唤醒空闲会话。
- **省略配置时仍默认使用 UTC**——不予采纳，因为显式启用的时钟应跟随部署环境，除非运维方选择 UTC。需要 UTC 的部署仍可配置 `timeZone: UTC`。
- **引入时区探测库**——不予采纳，因为 Node 的 `Intl` 运行时已经能够提供进程的 IANA 时区，而且额外依赖同样无法推断远程用户的时区。
- **在 `dsh-agent-spine-demo` 中挂载插件**——不予采纳，因为时区、信息披露、token 预算和新鲜度都属于部署策略。选择加入能保持默认上下文稳定。
- **将包放入 `core/`**——不予采纳，因为 `core/` 负责产品 API 主干，而该插件是没有服务键的可选叶节点。

## 后果

- 选择加入的模型无需调用工具，即可获得分区时钟和轮次间隔时长。每个请求的系统提示词成本固定，不会随会话增长。
- 省略 `timeZone` 时，插件采用加载时观察到的进程 `TZ`、主机或容器时区。当部署环境不能代表目标用户时，运维方必须显式配置时区。
- 刷新会改变请求头，并可能新增一份 reason 为 `change` 的完整 `request/header` 快照。`refreshIntervalMs` 用新鲜度换取完整持久快照的数量与大小；设为 `0` 时，每个整秒渲染结果发生变化的步骤都会记录新值。
- 系统不会仅为刷新时间而创建请求。长时间运行的工具会保留先前读数，直至下一步骤开始组装。
- 时长反映持久追加边界处的 harness 处理时间，不包含消息进入日志之前的客户端网络延迟。若要保留客户端来源时间戳，需要单独的持久输入契约。
