Optional time-context plugin
可选时间上下文插件
The dynamic system-prompt storage and refresh decision in this record is superseded by [Durable per-step time context](2026-07-16-durable-per-step-time-context.md). The opt-in package, zoned formatting, and validation remain; the follow-up owns the current model-visible and durability contract. An agent request has no live clock unless a deployment puts one in prompt text or gives the model a query tool. Static text
English
Problem
The dynamic system-prompt storage and refresh decision in this record is superseded by Durable per-step time context. The opt-in package, zoned formatting, and validation remain; the follow-up owns the current model-visible and durability contract.
An agent request has no live clock unless a deployment puts one in prompt text or gives the model a query tool. Static text becomes stale, while a tool call adds overhead to ordinary reasoning about dates, deadlines, or idle time. Without elapsed time, the model cannot distinguish an immediate follow-up from one sent hours after the preceding message.
Prompt assembly can derive both facts per step from durable session timestamps, and request-header logging can record the exact rendered value. Accumulating stale readings in conversation history or waking idle agents would violate the existing request lifecycle.
Decision
@deepseek-ai/dsh-time-context is an opt-in function plugin at packages/context/time-context/. The context/ product group holds bounded request-context enrichments that define neither a tool nor a service. dsh-agent-spine-demo and shipped examples do not load the package; deployments mount it explicitly when its token and disclosure costs are acceptable.
The plugin registers the global context:time system-prompt section at order 10, after the deployment persona and before tool guidance. For an active turn it emits an ISO-shaped timestamp with numeric UTC offset and IANA zone, plus a compact whole-second duration since the last model-visible message before the turn opened. Bare and idle assemblies receive an empty section.
Previous-message baseline
At a turn's first assembly, the provider scans before turn/start for the latest user/message, assistant/message, tool/result, context/message, or steering/message. It excludes the current prompt so the duration expresses the inter-turn gap instead of approximately zero. Every refresh in that turn keeps the same baseline, and the first turn reports unavailable (no earlier message in this session).
The baseline is the session event's append time, not an unlogged client timestamp. Resume and fork behavior are therefore deterministic from the durable log, and the model-visible value remains reconstructable without a new event. A backward wall-clock adjustment clamps the duration to zero.
Refresh policy
refreshIntervalMs defaults to 60,000 and must be a non-negative safe integer. Every turn's first request refreshes. Later assemblies in that turn reuse the block until its age reaches the interval; 0 refreshes every step. No timer creates work during model calls, tools, or idle time because refresh is request-bound.
When timeZone is omitted, Intl.DateTimeFormat resolves the Node process's system zone once at plugin load. Node honors TZ; without that override, the host or container supplies the zone. An explicit value must be an IANA identifier and is validated at load. The captured zone remains stable until plugin reload, and the ISO-shaped local timestamp includes its current numeric offset so daylight-saving changes stay explicit. This is the deployment process's zone, not a remote user's zone.
Logging and token shape
The loop records the temporal block in full request/header snapshots before transmission, satisfying the reconstructable-requests contract. Each request carries one current block; earlier readings do not remain in conversation history. The plugin owns the fact and contributes it through the prompt registry, following the prompt-variables Agent Note without a loop special case.
Testing
Unit tests pin formatting, baselines, refresh policy, validation, per-agent state, disposal, and load-time system-zone capture. A real agent-loop test pins the transmitted prompt and full request/header snapshots. A keyless subprocess e2e boots a test-only cordis.yml through the real Loader and stdio app, omits timeZone under a controlled TZ, drives two turns, and verifies the persisted request headers externally. Default snapshot compositions omit the plugin, so their transcript fixtures contain no temporal block.
Alternatives considered
- Append a
context/messageon every turn or refresh — rejected because readings and token cost would accumulate in history. Replacing a prior surface node would preserve its old position, while replacing the tail would hide intervening conversation. - Use
agent/session-prefix— rejected because the session-stable prefix cannot represent a per-turn or per-step clock. - Mutate requests in
agent/request— rejected because that seam shapes call config after the message boundary; inserted model content would bypass prompt-pressure accounting and request-header logging. - Register separate
{{current_time}}and{{elapsed}}variables — rejected because independent providers can sample different instants and require shared caching. One section records the pair atomically without a deployment-authored template. - Refresh from a background timer — rejected because a new value has no consumer outside request assembly. Timer-driven
agent.inject()would create turns and wake idle sessions merely to report time passing. - Keep UTC as the omitted default — rejected because an explicitly enabled clock should follow its deployment environment unless the operator chooses UTC.
timeZone: UTCremains available when a deployment requires it. - Add a time-zone detection library — rejected because Node's
Intlruntime already exposes the process's IANA zone. Another dependency cannot infer a remote user's zone either. - Mount the plugin in
dsh-agent-spine-demo— rejected because time zone, disclosure, token budget, and freshness are deployment policy. Opt-in keeps default context stable. - Place the package in
core/— rejected becausecore/owns the product API spine, while this plugin is an optional leaf with no service key.
Consequences
- Opted-in models receive a zoned clock and inter-turn duration without a tool call. The system-prompt cost is fixed per request instead of growing with the session.
- An omitted
timeZonefollows the process'sTZ, host, or container zone as observed at plugin load. Operators must configure an explicit zone when the deployment environment does not represent the intended user. - A refresh changes the request header and can add a full
request/headersnapshot with reasonchange.refreshIntervalMstrades freshness against the number and size of durable full snapshots;0records a new value on every step whose whole-second rendering changes. - No request exists solely to refresh time. A long-running tool leaves the prior reading until the next step assembles.
- Duration reflects harness processing time at durable append boundaries, not client-network latency before logging. Preserving a client-origin timestamp requires a separate durable input contract.
中文
问题
本记录中的动态系统提示词存储和刷新决策已由持久的逐步骤时间上下文取代。需要显式启用的包(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 快照记录时间区块,从而满足可重建请求契约。每个请求只携带一个当前区块;先前的读数不会保留在会话历史中。该插件拥有时间信息,并按照提示词变量 Agent Note通过提示词注册表贡献该信息,无需为循环添加特殊分支。
测试
单元测试固定格式化、基线、刷新策略、校验、逐 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 处理时间,不包含消息进入日志之前的客户端网络延迟。若要保留客户端来源时间戳,需要单独的持久输入契约。