From d9d9487e0e000b992f07edf8aad01cf78c960826 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 14 Jul 2026 17:44:32 +0800 Subject: [PATCH] docs: align time-context prose standard --- AGENTS.md | 4 +- docs/config-catalog.md | 4 +- .../2026-07-14-time-context-plugin.i18n.yaml | 4 +- .../feature/2026-07-14-time-context-plugin.md | 42 +++++++++---------- .../2026-07-14-time-context-plugin.zh.md | 42 +++++++++---------- packages/context/README.md | 4 +- packages/context/time-context/README.md | 12 +++--- packages/context/time-context/package.json | 2 +- packages/context/time-context/src/index.ts | 27 ++++-------- .../time-context/tests/time-context.spec.ts | 10 ----- 10 files changed, 66 insertions(+), 85 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 10ed41baca..44aa15aea5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,7 +18,7 @@ packages/ Harness packages at packages///, all named @deepseek-ai skill/ skill provider registry + local impl + catalog/loader tool web/ web seam + search/fetch providers + model-facing web tools compact/ compaction seam + basic backend - context/ optional bounded model-context enrichments + context/ request-context plugins subagent/ subagent seam + spawn/fork/ACP backends + delegation tool workflow/ workflow seam + worker-thread engine + the workflow tool todo/ the todo_write tool @@ -35,7 +35,7 @@ docs/ architecture, generated catalogs, RFCs, postmortems, cookbook (see scripts/ repo gates and generators ``` -Per-package map: the group READMEs, indexed from [packages/README.md](packages/README.md). +Package groups: [packages/README.md](packages/README.md). ## Commands diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 615de5051f..650fe16d23 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -785,7 +785,7 @@ Source: [`packages/core/system-prompt/src/index.ts:147`](../packages/core/system Requires: `systemPrompt` ```ts config-catalog -/** Configuration for the request-time clock section. */ +/** Request-time clock formatting and refresh policy. Invalid values fail plugin load. */ export interface Config { /** IANA time zone used for the rendered timestamp (default `UTC`). */ timeZone?: string @@ -794,7 +794,7 @@ export interface Config { } ``` -Source: [`packages/context/time-context/src/index.ts:28`](../packages/context/time-context/src/index.ts) +Source: [`packages/context/time-context/src/index.ts:22`](../packages/context/time-context/src/index.ts) ## `@deepseek-ai/dsh-tool-cordis` diff --git a/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.i18n.yaml b/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.i18n.yaml index 5ca24553a5..10b441f03b 100644 --- a/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.i18n.yaml +++ b/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-14-time-context-plugin.md: cab55c78c7f649db63d3049f9a1dfa8e0a6673e9 -2026-07-14-time-context-plugin.zh.md: a4644d8484c8c6f906a1123502c7530f09bcb1d8 +2026-07-14-time-context-plugin.md: 54ed3188c794eb088db47b653ea0e27db1c14ed8 +2026-07-14-time-context-plugin.zh.md: fa0de3c6460eb6ab508b53e1ca3acf99564b1926 diff --git a/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.md b/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.md index cab55c78c7..54ed3188c7 100644 --- a/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.md +++ b/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.md @@ -6,49 +6,49 @@ English | [中文](2026-07-14-time-context-plugin.zh.md) ## Problem -An agent request has no live clock unless a deployment hard-codes one into prompt text or gives the model a tool to query it. Static text becomes false immediately, while a tool call is unnecessary overhead for ordinary reasoning about dates, deadlines, or how long a conversation has been idle. The missing companion fact is elapsed time: the model receives the current user prompt but cannot distinguish a quick follow-up from one sent hours after the preceding conversation message. +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. -The prompt assembly and session log already provide the necessary inputs. A section provider runs once per step with the active agent, model-visible session events carry durable append timestamps, and the request-header fold records the exact rendered system prompt. The design question is where temporal facts belong and how often they change without accumulating stale readings or creating background work. +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 optional function plugin at `packages/context/time-context/`. It opens the `context/` product group for bounded request-context enrichments that define neither a tool nor a service seam. The package is not loaded by `dsh-agent-core` or a shipped example; a deployment mounts it explicitly when temporal context is worth the tokens and disclosure. +`@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-core` and shipped examples do not load the package; deployments mount it explicitly when its token and disclosure costs are acceptable. -The plugin registers one global `ctx.systemPrompt.section()` contribution named `context:time` at order 10, after the deployment persona and before tool guidance. Its provider returns two lines for an active agent turn: an ISO-shaped timestamp with numeric UTC offset and IANA zone, and a compact whole-second duration since the last model-visible message before that turn opened. A bare or idle prompt assembly receives an empty section. +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 backward from that turn's `turn/start` and uses the latest `user/message`, `assistant/message`, `tool/result`, `context/message`, or `steering/message` timestamp. It deliberately excludes the current turn's newly appended user prompt: measuring from that event would make the first request report approximately zero and lose the inter-turn gap the feature exists to convey. Every later refresh in the same turn retains the baseline, so a long-running turn reports the growing duration since the preceding conversation message. The first turn reports `unavailable (no earlier message in this session)`. +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 receipt time. That makes resume and fork behavior deterministic from the durable log and keeps the model-visible value reconstructable without introducing a new event. A backward wall-clock adjustment clamps the displayed duration to zero rather than producing a negative interval. +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 regardless of the prior turn's timestamp. Within a multi-step turn, a later assembly reuses the cached block until its age reaches the interval; `0` refreshes every step. The policy is request-bound: no timer creates work while the agent is inside a model call, running a tool, or idle, because no request exists to consume a new value. +`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. -`timeZone` defaults to `UTC` and is validated as an IANA identifier at plugin load. The formatter emits an ISO-shaped local timestamp including the resolved zone and its current numeric offset, so daylight-saving changes remain explicit instead of silently shifting a zone-less clock. +`timeZone` defaults to `UTC` and is validated as an IANA identifier at plugin load. The ISO-shaped local timestamp includes the resolved zone and current numeric offset, making daylight-saving changes explicit. ### Logging and token shape -The temporal block is dynamic system-prompt state. The loop's existing `request/header` snapshot and `request/header-delta` fold records every rendered change before transmission, satisfying the [reconstructable-requests contract](../architecture/2026-07-05-reconstructable-requests.md). A request carries exactly one current block; previous readings do not remain in derived conversation history. This follows the ownership rule in the [prompt-variables RFC](../architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md): the optional plugin owns the temporal fact and contributes it through the ordinary prompt registry, with no loop special case. +The loop records the temporal block through `request/header` and `request/header-delta` before transmission, satisfying the [reconstructable-requests contract](../architecture/2026-07-05-reconstructable-requests.md). 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 RFC](../architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md) without a loop special case. ## Testing -The package suite uses fake system time and covers UTC and offset formatting, first-turn fallback, every eligible previous-message variant, whole-duration units, backward-clock clamping, per-turn refresh, interval reuse, interval expiry, `0` per-step behavior, independent per-agent caches, invalid config, HMR disposal, and the Loader namespace path. A real agent-loop test pins the transmitted system prompt and its `request/header-delta` refresh record. No default snapshot changes because the plugin is intentionally absent from every shipped composition; mounting it in a default snapshot fixture would violate the opt-in decision. +Unit tests pin formatting, baselines, refresh policy, validation, per-agent state, and disposal. A real agent-loop test pins the transmitted prompt and `request/header-delta`; a Loader test pins the named-export path. Default snapshot compositions omit the plugin, so their transcript fixtures contain no temporal block. ## Alternatives considered -- **Append a `context/message` on every turn or refresh** — rejected: each reading remains in derived history, so stale clock values and token cost accumulate with conversation length. A surface replacement cannot both remove the old node and move the new reading to the tail; replacement preserves the old node's position, while replacing through the tail would hide intervening conversation. -- **Use `agent/session-prefix`** — rejected: the prefix is composed once per loop instance and is intentionally session-stable, so it cannot represent a clock that changes per turn or step. -- **Mutate requests in `agent/request`** — rejected: that seam shapes call config only, fires after the message boundary, and model-visible content inserted there would bypass both prompt-pressure accounting and the logged-header contract. -- **Register separate `{{current_time}}` and `{{elapsed}}` prompt variables** — rejected: independent providers can sample different instants and need shared caching to keep refresh semantics atomic. One section provider computes and records the pair as one value; deployments do not need to repeat a temporal template in their persona. -- **Inject from a background timer at the configured interval** — rejected: while no model request is being assembled, a fresh value has no consumer. Timer-driven `agent.inject()` would create durable one-shot turns and wake or mutate idle sessions merely to announce time passing. -- **Mount the plugin in `dsh-agent-core`** — rejected: time zone, disclosure, token budget, and desired freshness are deployment policy. Explicit opt-in keeps the default harness context stable. -- **Place the package in `core/`** — rejected: core owns the product API spine. A context enrichment is an optional leaf with no service key, so the dedicated group states its composition role directly. +- **Append a `context/message` on 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. +- **Mount the plugin in `dsh-agent-core`** — rejected because time zone, disclosure, token budget, and freshness are deployment policy. Opt-in keeps default context stable. +- **Place the package in `core/`** — rejected because `core/` owns the product API spine, while this plugin is an optional leaf with no service key. ## Consequences -- Models in opted-in deployments receive an unambiguous zoned clock and an inter-turn elapsed duration without spending a tool call. The system-prompt token cost is fixed per request instead of growing with the session. -- A refresh changes the request header and therefore adds a `request/header-delta` event. `refreshIntervalMs` trades clock freshness against those durable deltas; setting it to zero intentionally records a new value on every step whose whole-second rendering changed. -- No request is created solely to refresh time. A tool that runs longer than the interval leaves the prior reading in place until the next step assembles, when the provider catches up. -- The duration reflects harness processing time at durable append boundaries, not client-network latency before the message entered the log. Preserving a client-origin timestamp would require a separate durable input contract and is outside this plugin. +- 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. +- A refresh changes the request header and can add a `request/header-delta`. `refreshIntervalMs` trades freshness against durable deltas; `0` records 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. diff --git a/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.zh.md b/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.zh.md index a4644d8484..fa0de3c646 100644 --- a/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.zh.md +++ b/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.zh.md @@ -6,49 +6,49 @@ Status: implemented ## 问题 -如果部署方既没有将当前时间硬编码到提示词中,也没有向模型提供查询时间的工具,agent(智能体)请求就无法获得实时准确的时钟信息。静态文本会立即失真,而对于日期、截止时间或会话闲置时长等常规推理,调用工具会带来不必要的开销。模型还缺少另一项配套信息:已经过去的时长。模型虽然能收到当前用户提示词,却无法区分紧接着发送的消息与上一条会话消息几小时后才发送的消息。 +如果部署方既未在提示词中提供时钟,也未给模型提供查询工具,agent(智能体)请求就无法获得实时准确的时间。静态文本会变得陈旧,而对于日期、截止时间或闲置时长等常规推理,调用工具会增加开销。缺少已经过去的时长时,模型无法区分紧接着发送的消息与上一条消息几小时后才发送的消息。 -提示词组装流程和会话日志已经提供所需输入。区段提供方会在每个步骤中针对活跃 agent 运行一次;模型可见的会话事件带有持久的追加时间戳;请求头折叠结果会记录系统提示词实际渲染的确切内容。设计需要决定时间信息应归属何处、应以多高频率变化,同时避免累积陈旧读数或创建后台任务。 +提示词组装流程可以在每个步骤中根据持久会话时间戳派生这两项信息,请求头日志则可以记录实际渲染的确切值。在会话历史中累积陈旧读数或唤醒空闲 agent 都会违反现有请求生命周期。 ## 决策 -`@deepseek-ai/dsh-time-context` 是位于 `packages/context/time-context/` 的可选函数式插件。它新增 `context/` 产品分组,用于容纳既不定义工具、也不定义服务边界的有界请求上下文增强。`dsh-agent-core` 和仓库提供的任何示例都不会加载该 package;只有当时间上下文值得占用 token 和披露信息时,部署方才显式挂载它。 +`@deepseek-ai/dsh-time-context` 是位于 `packages/context/time-context/`、需要显式启用的函数插件。`context/` 产品分组用于容纳既不定义工具、也不定义服务的有界请求上下文增强。`dsh-agent-core` 和仓库提供的示例都不会加载该 package;只有当 token 与信息披露成本可接受时,部署方才显式挂载它。 -该插件注册一个名为 `context:time`、顺序值为 10 的全局 `ctx.systemPrompt.section()` 贡献,位置在部署方角色设定之后、工具指导之前。对于处于活跃轮次中的 agent,其提供方返回两行内容:一行是带数字 UTC 偏移和 IANA 时区、形似 ISO 的时间戳;另一行是从该轮次开始前最后一条模型可见消息起算的紧凑整秒时长。未绑定 agent 或 agent 处于空闲状态时,提示词组装结果中的该区段为空。 +该插件注册顺序值为 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)`。 +在轮次首次组装时,提供方会在 `turn/start` 之前查找最近的 `user/message`、`assistant/message`、`tool/result`、`context/message` 或 `steering/message`。它会排除当前提示词,使时长表达轮次间隔,而不是接近零。同一轮次中的每次刷新都保留这条基线;首个轮次报告 `unavailable (no earlier message in this session)`。 -基线采用会话事件的追加时间,而不是日志中不存在的客户端接收时间。这样,恢复和 fork 行为都能从持久日志中确定性重现,模型可见值也无需新增事件即可重建。如果系统挂钟向后调整,插件会将显示时长钳制为零,而不会产生负数间隔。 +基线采用会话事件的追加时间,而不是日志中不存在的客户端时间戳。因此,恢复和 fork 行为可以从持久日志中确定性重现,模型可见值也无需新增事件即可重建。系统挂钟向后调整时,插件会将时长钳制为零。 ### 刷新策略 -`refreshIntervalMs` 默认值为 60,000,并且必须是非负安全整数。每个轮次的首次请求都强制刷新,不受上一轮次时间戳影响。在包含多个步骤的轮次内,后续组装会复用缓存区块,直至其存在时间达到该间隔;设为 `0` 时每个步骤都刷新。该策略仅由请求驱动:agent 正在等待模型调用、运行工具或处于空闲状态时,没有请求会消费新值,因此计时器不会创建任何任务。 +`refreshIntervalMs` 默认值为 60,000,并且必须是非负安全整数。每个轮次的首次请求都会刷新。同一轮次中的后续组装会复用该区块,直至其存在时间达到该间隔;设为 `0` 时每个步骤都刷新。刷新仅由请求驱动,因此在模型调用、工具运行或空闲期间,计时器不会创建任务。 -`timeZone` 默认为 `UTC`,插件加载时会校验它是否为 IANA 标识符。格式化器会输出形似 ISO 的本地时间戳,其中包含已解析的时区及其当前数字偏移,使夏令时变化保持显式可见,而不是让不带时区的时钟在无提示的情况下发生偏移。 +`timeZone` 默认为 `UTC`,插件加载时会校验它是否为 IANA 标识符。形似 ISO 的本地时间戳包含已解析的时区及其当前数字偏移,使夏令时变化保持显式可见。 ### 日志与 token 形态 -时间区块属于动态系统提示词状态。agent loop(智能体循环)现有的 `request/header` 快照和 `request/header-delta` 折叠结果会在发送前记录每次渲染变化,从而满足[可重建请求契约](../architecture/2026-07-05-reconstructable-requests.md)。每个请求只携带一个当前区块;先前的读数不会保留在派生的会话历史中。该设计遵循[提示词变量 RFC](../architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md)的所有权规则:可选插件拥有时间信息,并通过常规提示词注册表贡献该信息,不为循环添加特殊分支。 +agent loop(智能体循环)会在发送前通过 `request/header` 和 `request/header-delta` 记录时间区块,从而满足[可重建请求契约](../architecture/2026-07-05-reconstructable-requests.md)。每个请求只携带一个当前区块;先前的读数不会保留在会话历史中。该插件拥有时间信息,并按照[提示词变量 RFC](../architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md)通过提示词注册表贡献该信息,无需为循环添加特殊分支。 ## 测试 -该 package 的测试套件使用伪造的系统时间,覆盖 UTC 与偏移格式化、首轮次回退文本、所有符合条件的上一条消息类型、完整时长单位、挂钟回拨钳制、逐轮次刷新、间隔内复用、间隔到期、`0` 对应的逐步骤行为、相互独立的逐 agent 缓存、无效配置、HMR(热模块替换)资源释放以及 Loader 命名空间路径。一个使用真实 agent loop 的测试会固定实际发送的系统提示词及其 `request/header-delta` 刷新记录。默认快照不发生变化,因为所有仓库提供的组合都刻意不包含该插件;若在默认快照 fixture 中挂载它,将违反显式选择加入的决策。 +单元测试固定格式化、基线、刷新策略、校验、逐 agent 状态和资源释放行为。使用真实 agent loop 的测试固定实际发送的提示词和 `request/header-delta`;Loader 测试固定命名导出路径。默认快照组合不包含该插件,因此其中的 transcript(文本记录)fixture(测试前置数据)不包含时间区块。 ## 考虑过的替代方案 -- **每个轮次或每次刷新都追加一条 `context/message`**——不予采纳:每个读数都会留在派生历史中,因此陈旧时钟值和 token 成本会随会话长度累积。表层替换操作无法同时删除旧节点并将新读数移动到尾部;替换会保留旧节点的位置,而通过尾部节点替换又会隐藏中间的会话内容。 -- **使用 `agent/session-prefix`**——不予采纳:前缀在每个循环实例中只组装一次,并且按设计在会话期间保持稳定,因此无法表示每个轮次或步骤都会变化的时钟。 -- **在 `agent/request` 中修改请求**——不予采纳:该边界只负责塑造调用配置,触发时间晚于消息边界;如果在此处插入模型可见内容,会同时绕过提示词压力核算和请求头日志契约。 -- **注册独立的 `{{current_time}}` 和 `{{elapsed}}` 提示词变量**——不予采纳:两个独立提供方可能在不同时间点采样,并且需要共享缓存才能保证刷新语义的原子性。单个区段提供方将二者作为一个值计算和记录;部署方也不需要在角色设定中重复时间模板。 -- **按照配置的间隔通过后台计时器注入**——不予采纳:没有正在组装的模型请求时,新值没有消费方。由计时器驱动 `agent.inject()` 会创建持久的一次性轮次,并且只为通知时间流逝就唤醒或修改空闲会话。 -- **在 `dsh-agent-core` 中挂载插件**——不予采纳:时区、信息披露、token 预算和期望新鲜度都属于部署策略。显式选择加入能保持默认 harness 上下文稳定。 -- **将 package 放入 `core/`**——不予采纳:core 负责产品 API 主干。上下文增强是没有服务键的可选叶节点,因此专用分组能直接表达其组合角色。 +- **每个轮次或每次刷新都追加一条 `context/message`**——不予采纳,因为读数和 token 成本会在历史中累积。替换先前的表层节点会保留其旧位置,而替换尾部节点会隐藏中间的会话内容。 +- **使用 `agent/session-prefix`**——不予采纳,因为会话期间保持稳定的前缀无法表示逐轮次或逐步骤变化的时钟。 +- **在 `agent/request` 中修改请求**——不予采纳,因为该边界在消息边界之后塑造调用配置;插入模型可见内容会绕过提示词压力核算和请求头日志。 +- **注册独立的 `{{current_time}}` 和 `{{elapsed}}` 变量**——不予采纳,因为独立提供方可能在不同时间点采样,并且需要共享缓存。单个区段会以原子方式记录两项信息,也不需要部署方编写时间模板。 +- **通过后台计时器刷新**——不予采纳,因为请求组装之外没有消费新值的对象。由计时器驱动 `agent.inject()` 会创建轮次,并且只为报告时间流逝就唤醒空闲会话。 +- **在 `dsh-agent-core` 中挂载插件**——不予采纳,因为时区、信息披露、token 预算和新鲜度都属于部署策略。选择加入能保持默认上下文稳定。 +- **将 package 放入 `core/`**——不予采纳,因为 `core/` 负责产品 API 主干,而该插件是没有服务键的可选叶节点。 ## 后果 -- 选择加入的部署无需消耗工具调用,即可让模型获得无歧义的分区时钟和轮次间隔时长。每个请求的系统提示词 token 成本固定,不会随会话增长。 -- 刷新会改变请求头,因此会新增一条 `request/header-delta` 事件。`refreshIntervalMs` 用时钟新鲜度换取这些持久增量记录的数量;将其设为零会在每个整秒渲染结果发生变化的步骤中刻意记录新值。 -- 系统不会仅为刷新时间而创建请求。工具运行时间超过该间隔时,先前读数会保持不变,直至下一步骤开始组装,此时提供方会追赶到当前时间。 -- 时长反映持久追加边界处的 harness 处理时间,不包含消息进入日志之前的客户端网络延迟。若要保留客户端来源时间戳,需要单独的持久输入契约,不属于本插件范围。 +- 选择加入的模型无需调用工具,即可获得分区时钟和轮次间隔时长。每个请求的系统提示词成本固定,不会随会话增长。 +- 刷新会改变请求头,并可能新增 `request/header-delta`。`refreshIntervalMs` 用新鲜度换取持久增量记录的数量;设为 `0` 时,每个整秒渲染结果发生变化的步骤都会记录新值。 +- 系统不会仅为刷新时间而创建请求。长时间运行的工具会保留先前读数,直至下一步骤开始组装。 +- 时长反映持久追加边界处的 harness 处理时间,不包含消息进入日志之前的客户端网络延迟。若要保留客户端来源时间戳,需要单独的持久输入契约。 diff --git a/packages/context/README.md b/packages/context/README.md index 00755fce26..49a297d11a 100644 --- a/packages/context/README.md +++ b/packages/context/README.md @@ -1,7 +1,7 @@ # context/ — optional request context -Product plugins that add bounded model-visible request context without defining a tool or service seam. They are opt-in deployment leaves and are not part of the default `dsh-agent-core` bundle. +Opt-in plugins that add bounded model-visible request context without defining a tool or service. The default `dsh-agent-core` bundle excludes them. | Package | Role | ctx key | |---|---|---| -| `time-context/` | Dynamic current time and elapsed-since-previous-message system-prompt section | (none) | +| `time-context/` | Current time and elapsed-time system-prompt context | (none) | diff --git a/packages/context/time-context/README.md b/packages/context/time-context/README.md index 3d9ffdd656..445101f4f6 100644 --- a/packages/context/time-context/README.md +++ b/packages/context/time-context/README.md @@ -1,6 +1,6 @@ # @deepseek-ai/dsh-time-context -Optional temporal request context. The plugin contributes one dynamic system-prompt section with the current zoned time and the elapsed duration since the last model-visible message before the current turn. It is not mounted by `dsh-agent-core` or any shipped example; deployments opt in explicitly. Decision record: [the time-context RFC](../../../docs/rfc/implemented/feature/2026-07-14-time-context-plugin.md). +Opt-in dynamic system-prompt context with the current zoned time and elapsed time since the latest model-visible message before the turn. `dsh-agent-core` and shipped examples do not mount it. Decision record: [the time-context RFC](../../../docs/rfc/implemented/feature/2026-07-14-time-context-plugin.md). ## Config @@ -12,21 +12,21 @@ Optional temporal request context. The plugin contributes one dynamic system-pro refreshIntervalMs: 60000 # default; 0 refreshes on every step ``` -`timeZone` is validated at plugin load. `refreshIntervalMs` must be a non-negative safe integer and is evaluated only when a request is assembled: every turn's first request gets a fresh reading, and a later step in the same turn reuses that reading until it is at least this old. Thus `0` means per-step refresh, while a positive value bounds staleness at request boundaries without creating timer-driven turns. +`timeZone` is validated at plugin load. `refreshIntervalMs` must be a non-negative safe integer. Every turn's first request refreshes; later steps reuse the reading until its age reaches the interval. `0` refreshes every step. Refresh occurs only during request assembly and creates no timer work. ## Message baseline -The duration starts at the latest model-visible session event before the current `turn/start`: a user, assistant, tool-result, context, or steering message. All later refreshes in that turn retain the same baseline, so the value measures elapsed time since the preceding conversation message rather than collapsing to approximately zero after the current prompt is appended. The first turn reports that no earlier message exists. Session event append time is the durable clock source; client-side send time is not part of the session contract. +The duration starts at the latest user, assistant, tool-result, context, or steering message before the current `turn/start`. Every refresh in the turn retains that baseline, so the current prompt does not collapse the interval to approximately zero. The first turn reports that no earlier message exists. The durable clock source is session-event append time, not client send time. -The plugin uses a dynamic system-prompt section rather than retained `context/message` history. The loop records the exact rendered value in `request/header` / `request/header-delta`, so requests remain reconstructable while the current request carries only one timing block. +The loop records the dynamic section in `request/header` / `request/header-delta`. Requests therefore remain reconstructable, carry one timing block, and retain no earlier readings in conversation history. ## Model Experience ### Temporal system prompt -**What the model sees**: Every request in an active turn includes the two-line section below. `` is an ISO-shaped local timestamp with numeric offset and IANA zone; `` is compact whole-second units or the first-turn fallback. +**What the model sees**: Every request in an active turn includes the two lines below. `` is an ISO-shaped local timestamp with numeric offset and IANA zone; `` is compact whole-second units or the first-turn fallback. -**Token effect**: Fixed two-line request context. A refresh replaces the section in the request header rather than retaining prior readings in conversation history. +**Token effect**: Fixed two-line cost per request. A refresh replaces the request-header section; prior readings do not accumulate. #### Temporal context section diff --git a/packages/context/time-context/package.json b/packages/context/time-context/package.json index 202976ccf8..e18bb32540 100644 --- a/packages/context/time-context/package.json +++ b/packages/context/time-context/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-time-context", - "description": "Optional dynamic system-prompt context with the current time and elapsed duration since the previous message", + "description": "Opt-in system-prompt context with the current time and elapsed time since the previous message", "version": "0.0.1", "private": true, "type": "module", diff --git a/packages/context/time-context/src/index.ts b/packages/context/time-context/src/index.ts index 9162ef8614..7b087bc4de 100644 --- a/packages/context/time-context/src/index.ts +++ b/packages/context/time-context/src/index.ts @@ -1,14 +1,8 @@ /** - * Optional temporal context for model requests. The plugin contributes one - * dynamic system-prompt section that reports the current zoned time and the - * elapsed duration since the last model-visible message before the current - * turn. A turn always gets a fresh reading on its first request; later steps - * refresh only when the configured maximum age is reached. - * - * The section is request state, not retained conversation history. The agent - * loop records each rendered value through its existing `request/header` or - * `request/header-delta` event, preserving the model-visible/logged invariant - * without accumulating stale `context/message` entries. + * Opt-in request-time clock context. Active turns receive the current zoned + * time and elapsed time since the preceding model-visible message. The loop + * logs each rendered value as request-header state rather than conversation + * history. * * @module @deepseek-ai/dsh-time-context */ @@ -24,7 +18,7 @@ export const name = 'time-context' /** The system-prompt registry that owns the dynamic request section. */ export const inject = ['systemPrompt'] -/** Configuration for the request-time clock section. */ +/** Request-time clock formatting and refresh policy. Invalid values fail plugin load. */ export interface Config { /** IANA time zone used for the rendered timestamp (default `UTC`). */ timeZone?: string @@ -38,13 +32,12 @@ export const Config: z = z.object({ refreshIntervalMs: z.number().default(60_000), }) -/** The open turn currently being assembled, including its log boundary. */ interface OpenTurn { turn: number startSeq: number } -/** One agent's last rendered block and its fixed previous-turn baseline. */ +/** Cached text and the fixed inter-turn baseline used by one agent's open turn. */ interface RenderState { turn: number renderedAt: number @@ -52,10 +45,8 @@ interface RenderState { text: string } -/** Date-time fields required from the fixed formatter below. */ type TimestampPart = 'day' | 'hour' | 'minute' | 'month' | 'second' | 'timeZoneName' | 'year' -/** Find the open turn at the tail of an agent's balanced session log. */ function openTurn(agent: Agent): OpenTurn | undefined { for (const event of [...agent.session.events].reverse()) { switch (event.type) { @@ -71,7 +62,7 @@ function openTurn(agent: Agent): OpenTurn | undefined { return undefined } -/** Timestamp of the last model-visible message before one turn opened. */ +/** Find the latest model-visible timestamp strictly before one turn boundary. */ function previousMessageTime(agent: Agent, turnStartSeq: number): number | undefined { for (const event of [...agent.session.events].reverse()) { if (event.seq >= turnStartSeq) continue @@ -116,7 +107,6 @@ function formatDuration(elapsedMs: number): string { return parts.join(' ') } -/** Build the exact two-line model-facing section. */ function renderText( now: number, previous: number | undefined, @@ -130,9 +120,10 @@ function renderText( } /** - * Register the dynamic temporal system-prompt section. + * Register the request-time clock section for the lifetime of `ctx`. * @param ctx - plugin context; the section registration is disposed with it. * @param config - validated time zone and intra-turn refresh interval. + * @throws when the time zone or refresh interval is invalid. */ export function apply(ctx: Context, config: Config): void { const timeZone = config.timeZone as string diff --git a/packages/context/time-context/tests/time-context.spec.ts b/packages/context/time-context/tests/time-context.spec.ts index c035a0594f..8cf9e71f53 100644 --- a/packages/context/time-context/tests/time-context.spec.ts +++ b/packages/context/time-context/tests/time-context.spec.ts @@ -1,5 +1,3 @@ -/** Unit, loop-integration, lifecycle, and real-Loader coverage for dsh-time-context. */ - import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { Context } from 'cordis' import Loader from '@cordisjs/plugin-loader' @@ -25,7 +23,6 @@ afterEach(() => { vi.useRealTimers() }) -/** Mount the system-prompt service and the optional plugin. */ async function mount(config: Config = {}) { const ctx = new Context() await ctx.plugin(SystemPrompt) @@ -33,18 +30,15 @@ async function mount(config: Config = {}) { return { ctx, fiber } } -/** Minimal agent-shaped holder over a real append-only Session. */ function sessionAgent(session: Session, id = 'agent'): Agent { return { id: AgentId(id), session } as unknown as Agent } -/** Resolve only this plugin's assembled section text. */ async function sectionText(ctx: Context, agent?: Agent): Promise { const assembly = await ctx.systemPrompt.assemble(agent === undefined ? {} : { agent }) return assembly.sections.find(section => section.name === 'context:time')?.text } -/** Append the prompt side of an open message turn. */ function openMessageTurn(session: Session, turn: number): void { session.append('turn/start', { turn, trigger: { kind: 'message', source: { kind: 'user' } } }) session.append('user/message', { @@ -53,7 +47,6 @@ function openMessageTurn(session: Session, turn: number): void { }, { surfaceOp: 'append' }) } -/** Script helper for a text-only model response. */ function textResponse(text: string): StreamChunk[] { return [ { type: 'block-start', index: 0, blockType: 'text' }, @@ -62,7 +55,6 @@ function textResponse(text: string): StreamChunk[] { ] } -/** Script helper for one tool-call response. */ function toolCallResponse(): StreamChunk[] { return [ { type: 'block-start', index: 0, blockType: 'tool-call' }, @@ -75,7 +67,6 @@ function toolCallResponse(): StreamChunk[] { ] } -/** Deterministic adapter that records each request and consumes one chunk script. */ class ScriptedAdapter extends LlmAdapter { readonly requests: GenerateOptions[] = [] @@ -91,7 +82,6 @@ class ScriptedAdapter extends LlmAdapter { } } -/** Mount the real loop spine plus this optional plugin. */ async function loopHarness(adapter: ScriptedAdapter, config: Config = {}): Promise { const ctx = new Context() await ctx.plugin(LlmService)