@deepseek-ai/dsh-time-context 0.0.1-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +28 -0
- package/README.i18n.yaml +6 -0
- package/README.md +70 -0
- package/README.zh.md +70 -0
- package/lib/index.js +286 -0
- package/lib/invariant.js +74 -0
- package/lib/types/index.d.ts +29 -0
- package/lib/types/invariant.d.ts +13 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, DeepSeek
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/context/time-context/README.md
|
|
5
|
+
README.md: 9956918c63b49de8ec5e739bc3d9887e269930a8
|
|
6
|
+
README.zh.md: 3a9bb1012fc0639d9c3f6b104cea5a64d4b187d6
|
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-time-context
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
Opt-in durable context with the current zoned time and elapsed time sampled during model-request preparation. `dsh-agent-spine-demo` and shipped examples do not mount it. Decision record: [the durable time-context Agent Note](../../../.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.md).
|
|
6
|
+
|
|
7
|
+
## Config
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
- id: time-context
|
|
11
|
+
name: '@deepseek-ai/dsh-time-context'
|
|
12
|
+
config:
|
|
13
|
+
timeZone: Asia/Shanghai # optional IANA override; omit for the process zone
|
|
14
|
+
refreshIntervalMs: 60000 # optional; omit or set to 0 for every eligible attempt
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
When `timeZone` is omitted, the plugin 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 `timeZone` must be an IANA identifier and is validated at plugin load.
|
|
18
|
+
|
|
19
|
+
`refreshIntervalMs` must be a non-negative safe integer. Omission or `0` adds context to every eligible entering pre-step whose signal is not already aborted. A positive value adds it only when the session has no earlier time-context injection, wall time moved backward, or at least that many milliseconds have elapsed since the latest injection.
|
|
20
|
+
|
|
21
|
+
## Timing semantics
|
|
22
|
+
|
|
23
|
+
The plugin prepends an `agent/pre-step` listener. When an injection is due and the downstream decision enters the proposed step, it adds one sourced `UserMessage` to the returned batch. AgentLoop records that context after `step/start` and before ordinary automatic compaction with source `{ kind: 'plugin', plugin: 'time-context' }`. A suppressed, rejected, or failed pre-step records nothing.
|
|
24
|
+
|
|
25
|
+
Positive-interval scheduling scans the raw durable session events for the latest `user/message` with that source, including a reading shadowed by compaction. The schedule therefore applies across turns and resumed processes without process-local cache state. It reduces append frequency and history growth but never removes an existing reading, and sessions schedule independently.
|
|
26
|
+
|
|
27
|
+
Step 1 measures from the latest preceding model-visible message, including the prompt that opened the turn. Later steps measure from the preceding time-context event in the same turn. Both baselines use durable session-event timestamps; backward wall-clock movement clamps elapsed time to zero. A missing first-step baseline, or a later step with no earlier same-turn reading because interval suppression skipped it, reports `unavailable`.
|
|
28
|
+
|
|
29
|
+
A time reading records an entered pre-step batch, not a completed step or transmitted request. A later request-preparation failure can therefore leave the reading in history, but a downstream pre-step listener that rejects or fails prevents it from being recorded.
|
|
30
|
+
|
|
31
|
+
The separately published `./invariant` companion checks each plugin-attributed reading against the open turn, next pre-step position, elapsed baseline, and durable event time. Its rendered timestamp must parse and cannot postdate the event; process suspension between sampling and append does not invalidate the reading.
|
|
32
|
+
|
|
33
|
+
The time reading stays in derived conversation history until a later compaction shadows it. Request headers contain no time-context state. Request reconstruction uses the complete durable surface prefix after each `step/start`, so transmitted requests need not map one-to-one to readings: request preparation can fail after step entry, while interval suppression can let a request reuse existing history without adding one.
|
|
34
|
+
|
|
35
|
+
## Model Experience
|
|
36
|
+
|
|
37
|
+
### Preparation-time temporal context
|
|
38
|
+
|
|
39
|
+
#### What the model sees
|
|
40
|
+
|
|
41
|
+
On each preparation attempt that injects, one source-tagged context message containing the two lines below. `<timestamp>` is an ISO-shaped local timestamp with numeric offset and IANA zone; durations use compact whole-second units. Positive intervals can leave an attempted step without a new reading.
|
|
42
|
+
|
|
43
|
+
##### First step
|
|
44
|
+
|
|
45
|
+
```markdown
|
|
46
|
+
Time sampled while preparing turn <turn>, step 1: <timestamp>
|
|
47
|
+
Elapsed since the preceding model-visible message: <duration-or-unavailable>.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
##### Later steps
|
|
51
|
+
|
|
52
|
+
```markdown
|
|
53
|
+
Time sampled while preparing turn <turn>, step <step>: <timestamp>
|
|
54
|
+
Elapsed since the preceding step context: <duration-or-unavailable>.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
#### Token effect
|
|
58
|
+
|
|
59
|
+
Each injected two-line message accumulates until compaction shadows it. A positive interval reduces additions; omission or `0` adds one for every eligible preparation attempt.
|
|
60
|
+
|
|
61
|
+
#### KV Cache effect
|
|
62
|
+
|
|
63
|
+
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
|
|
64
|
+
|
|
65
|
+
## Known Limitations and Deferred Work
|
|
66
|
+
|
|
67
|
+
- **Whole-second display** — timestamps and durations omit sub-second precision even though durable event times retain milliseconds.
|
|
68
|
+
- **Session-event baseline** — elapsed time starts from durable append timestamps, not a client transport's original send timestamp.
|
|
69
|
+
- **Process-local default zone** — omission uses the Node process's `TZ`, host, or container zone captured at plugin load, not a remote user's zone; configure an explicit IANA zone when those differ.
|
|
70
|
+
- **History cost between compactions** — omission or `0` retains one reading for every eligible preparation attempt, including attempts later cancelled or failed; a positive interval reduces but does not eliminate this cost.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-time-context
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
可选的持久上下文,包含模型请求准备期间采样的带时区的当前时间与经过时长。`dsh-agent-spine-demo` 与随附示例不挂载该插件。决策记录:[持久 time-context Agent Note](../../../.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.md)。
|
|
6
|
+
|
|
7
|
+
## 配置
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
- id: time-context
|
|
11
|
+
name: '@deepseek-ai/dsh-time-context'
|
|
12
|
+
config:
|
|
13
|
+
timeZone: Asia/Shanghai # optional IANA override; omit for the process zone
|
|
14
|
+
refreshIntervalMs: 60000 # optional; omit or set to 0 for every eligible attempt
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
省略 `timeZone` 时,插件会在加载时解析一次 Node 进程的系统时区。Node 遵循 `TZ`;如果没有该覆盖,时区由宿主或容器提供。显式 `timeZone` 必须是 IANA 标识符,并在插件加载时验证。
|
|
18
|
+
|
|
19
|
+
`refreshIntervalMs` 必须是非负安全整数。省略或设为 `0` 时,会为每次信号尚未中止且会进入步骤的合格步骤前处理添加上下文。正数值只会在会话没有早先 time-context 注入、挂钟时间倒退,或自最新注入起已经过至少相应毫秒数时添加上下文。
|
|
20
|
+
|
|
21
|
+
## 时序语义
|
|
22
|
+
|
|
23
|
+
该插件会前置一个 `agent/pre-step` 监听器。需要注入且下游决策进入拟议步骤时,它会在返回批次中添加一条带来源的 `UserMessage`。AgentLoop 会在 `step/start` 之后、普通自动压缩(compaction)之前记录该上下文,其来源为 `{ kind: 'plugin', plugin: 'time-context' }`。被抑制、拒绝或失败的步骤前处理不会记录任何内容。
|
|
24
|
+
|
|
25
|
+
正间隔调度会扫描原始持久会话事件,查找最新的上述源 `user/message`,包括已被压缩遮蔽的时间读数。因此,调度可以跨轮次以及进程恢复持续生效,不需要进程本地缓存状态。它会降低追加频率与历史增长,但绝不移除现有时间读数,且每个会话独立调度。
|
|
26
|
+
|
|
27
|
+
第 1 步从前一条模型可见消息起测量,包括开启轮次的提示词。后续步骤从同一轮次中前一个 time-context 事件起测量。两种基线都使用持久会话事件时间戳;挂钟时间倒退时,经过时长限制为零。如果第一步缺少基线,或者后续步骤因间隔抑制而没有较早的同轮次时间读数,则报告 `unavailable`。
|
|
28
|
+
|
|
29
|
+
时间读数记录的是一个已进入步骤的步骤前批次,不是已完成步骤或已传输请求。后续请求准备失败时,该读数可能已留在历史中;但下游步骤前监听器拒绝或失败时,该读数不会被记录。
|
|
30
|
+
|
|
31
|
+
单独发布的 `./invariant` 配套模块会根据当前未结束的轮次、下一个步骤前位置、经过时长基线与持久事件时间检查每个归因于插件的时间读数。其渲染时间戳必须可解析,且不能晚于该事件;采样与追加之间的进程挂起不会使时间读数失效。
|
|
32
|
+
|
|
33
|
+
时间读数会保留在派生会话历史中,直到后续压缩遮蔽它。请求标头不含 time-context 状态。请求重建会在每个 `step/start` 之后使用完整持久表层前缀,因此已传输请求无需与时间读数一一对应:请求准备可能在进入步骤后失败,而间隔抑制可让请求复用现有历史,无需添加时间读数。
|
|
34
|
+
|
|
35
|
+
## 模型体验
|
|
36
|
+
|
|
37
|
+
### 准备期时间上下文
|
|
38
|
+
|
|
39
|
+
#### 模型看到的内容
|
|
40
|
+
|
|
41
|
+
每次执行注入的准备尝试都会生成一条带源标记的上下文消息,包含下方两行。`<timestamp>` 是带数字偏移与 IANA 时区、形如 ISO 的本地时间戳;持续时间使用紧凑的整秒单位。正间隔可能使某次步骤尝试没有新时间读数。
|
|
42
|
+
|
|
43
|
+
##### 第一步
|
|
44
|
+
|
|
45
|
+
```markdown
|
|
46
|
+
Time sampled while preparing turn <turn>, step 1: <timestamp>
|
|
47
|
+
Elapsed since the preceding model-visible message: <duration-or-unavailable>.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
##### 后续步骤
|
|
51
|
+
|
|
52
|
+
```markdown
|
|
53
|
+
Time sampled while preparing turn <turn>, step <step>: <timestamp>
|
|
54
|
+
Elapsed since the preceding step context: <duration-or-unavailable>.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
#### Token 影响
|
|
58
|
+
|
|
59
|
+
每条注入的两行消息都会累积,直到压缩遮蔽它。正间隔会减少添加;省略或设为 `0` 则会为每次合格准备尝试添加一条。
|
|
60
|
+
|
|
61
|
+
#### KV Cache 影响
|
|
62
|
+
|
|
63
|
+
仅追加;新可见内容位于可复用请求前缀之后,不会使现有 KV Cache 条目失效。
|
|
64
|
+
|
|
65
|
+
## 已知限制与暂缓事项
|
|
66
|
+
|
|
67
|
+
- **整秒显示**:时间戳与持续时间省略亚秒精度,尽管持久事件时间保留毫秒。
|
|
68
|
+
- **会话事件基线**:经过时长从持久追加时间戳起计算,而非客户端传输的原始发送时间戳。
|
|
69
|
+
- **进程本地默认时区**:省略设置时,使用插件加载时捕获的 Node 进程 `TZ`、宿主或容器时区,而非远程用户的时区;两者不同时,请配置显式 IANA 时区。
|
|
70
|
+
- **压缩之间的历史成本**:省略设置或设为 `0` 会为每次合格准备尝试保留一条时间读数,包括后续取消或失败的尝试;正间隔可以降低但无法消除该成本。
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import z from "@deepseek-ai/schemastery";
|
|
3
|
+
import "@deepseek-ai/cordis";
|
|
4
|
+
//#region ../../llm/llm/src/brand.ts
|
|
5
|
+
/**
|
|
6
|
+
* Brand a message identifier.
|
|
7
|
+
* @param id - the opaque message identifier.
|
|
8
|
+
* @returns the same string, branded; no validation is performed.
|
|
9
|
+
*/
|
|
10
|
+
function MessageId(id) {
|
|
11
|
+
return id;
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region ../../llm/llm/src/call-config.ts
|
|
15
|
+
/**
|
|
16
|
+
* Deep-freeze a value in place with an iterative traversal, guarding cycles,
|
|
17
|
+
* so later mutation throws without imposing a JavaScript call-stack depth cap.
|
|
18
|
+
* {@link AbortSignal} objects are deliberately skipped because they are the
|
|
19
|
+
* request's live cancellation channel and freezing them breaks abort.
|
|
20
|
+
* @param value - the value to freeze in place.
|
|
21
|
+
* @returns the same value, frozen.
|
|
22
|
+
*/
|
|
23
|
+
function deepFreeze(value) {
|
|
24
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
25
|
+
const pending = [{
|
|
26
|
+
kind: "visit",
|
|
27
|
+
node: value
|
|
28
|
+
}];
|
|
29
|
+
while (pending.length > 0) {
|
|
30
|
+
const task = pending.pop();
|
|
31
|
+
/* v8 ignore next -- the loop condition guarantees one pending task. */
|
|
32
|
+
if (task === void 0) continue;
|
|
33
|
+
if (task.kind === "property") {
|
|
34
|
+
pending.push({
|
|
35
|
+
kind: "visit",
|
|
36
|
+
node: task.source[task.key]
|
|
37
|
+
});
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const node = task.node;
|
|
41
|
+
if (node === null || typeof node !== "object") continue;
|
|
42
|
+
if (node instanceof AbortSignal) continue;
|
|
43
|
+
if (seen.has(node)) continue;
|
|
44
|
+
seen.add(node);
|
|
45
|
+
Object.freeze(node);
|
|
46
|
+
const keys = Object.keys(node);
|
|
47
|
+
for (let index = keys.length - 1; index >= 0; index--) {
|
|
48
|
+
const key = keys[index];
|
|
49
|
+
/* v8 ignore next -- the loop is bounded by the captured key count. */
|
|
50
|
+
if (key === void 0) continue;
|
|
51
|
+
pending.push({
|
|
52
|
+
kind: "property",
|
|
53
|
+
source: node,
|
|
54
|
+
key
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region ../../llm/llm/src/message.ts
|
|
62
|
+
/** Message value types, identity, and immutable construction helpers. */
|
|
63
|
+
/**
|
|
64
|
+
* Detach and deep-freeze a message whose identity already exists.
|
|
65
|
+
* @param message - complete message, including its stable identity.
|
|
66
|
+
* @returns an immutable snapshot that preserves the identity.
|
|
67
|
+
*/
|
|
68
|
+
function freezeMessage(message) {
|
|
69
|
+
return deepFreeze(structuredClone(message));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Create one identified message and freeze it before publication.
|
|
73
|
+
* @param input - complete role, content, and source for a new message.
|
|
74
|
+
* @returns an immutable message with a fresh stable identity.
|
|
75
|
+
*/
|
|
76
|
+
function createMessage(input) {
|
|
77
|
+
return freezeMessage({
|
|
78
|
+
...input,
|
|
79
|
+
id: MessageId(crypto.randomUUID())
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Create one identified user-role message and freeze it before publication.
|
|
84
|
+
* @param input - complete content and source for a new user message.
|
|
85
|
+
* @returns an immutable user message with a fresh stable identity.
|
|
86
|
+
*/
|
|
87
|
+
function createUserMessage(input) {
|
|
88
|
+
return createMessage({
|
|
89
|
+
...input,
|
|
90
|
+
role: "user"
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region ../../util/timeout/src/index.ts
|
|
95
|
+
/** Largest delay Node schedules without clamping it to one millisecond. */
|
|
96
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region ../../llm/llm/src/error.ts
|
|
99
|
+
/**
|
|
100
|
+
* Canonical provider-neutral code for a response that completed normally but
|
|
101
|
+
* carried no content blocks at all. Providers occasionally emit a degenerate
|
|
102
|
+
* completion (a terminal stop with zero output); adapters classify it as this
|
|
103
|
+
* failure instead of yielding an empty assistant message, because an empty
|
|
104
|
+
* message silently ends the turn with nothing for the user or the loop to act
|
|
105
|
+
* on. The attempt produced nothing durable, so retry policy treats it as safe
|
|
106
|
+
* to repeat.
|
|
107
|
+
*/
|
|
108
|
+
const EMPTY_RESPONSE_CODE = "EMPTY_RESPONSE";
|
|
109
|
+
new RegExp(String.raw`(?:^|[^a-z0-9])context[\s_-](?:length|window)[\s_-]` + String.raw`(?:exceed(?:ed|s)?|overflow(?:ed)?|limit[\s_-]exceeded)(?:$|[^a-z0-9])`, "i");
|
|
110
|
+
new RegExp(String.raw`\b(?:request|prompt|input|messages?)\s+(?:is\s+|are\s+)?` + String.raw`too\s+(?:large|long)\s+for\s+(?:(?:this|the)\s+)?` + String.raw`(?:model(?:'s)?\s+)?context(?:\s+window)?\b`, "i");
|
|
111
|
+
new RegExp(String.raw`\b(?:input|prompt|request|messages?)\b.{0,40}` + String.raw`\b(?:exceed(?:s|ed)?|overflows?|is\s+larger\s+than)\b.{0,40}` + String.raw`\b(?:the\s+)?(?:model(?:'s)?\s+)?context(?:\s+(?:length|window))?\b`, "i");
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region ../../llm/llm/src/retry-policy.ts
|
|
114
|
+
/**
|
|
115
|
+
* Provider-owned request-retry policy configuration and resolution.
|
|
116
|
+
*
|
|
117
|
+
* Adapters expose one resolved policy per registered provider route; the
|
|
118
|
+
* optional dsh-llm-retry plugin executes it on the agent's failed-step extension point.
|
|
119
|
+
*
|
|
120
|
+
* @module @deepseek-ai/dsh-llm/retry-policy
|
|
121
|
+
*/
|
|
122
|
+
const DEFAULT_MAX_RETRIES = 2;
|
|
123
|
+
const DEFAULT_INITIAL_DELAY_MS = 500;
|
|
124
|
+
const DEFAULT_MAX_DELAY_MS = 1e4;
|
|
125
|
+
const DEFAULT_JITTER_RATIO = .1;
|
|
126
|
+
const DEFAULT_RETRYABLE_CODES = Object.freeze([
|
|
127
|
+
EMPTY_RESPONSE_CODE,
|
|
128
|
+
"RATE_LIMIT",
|
|
129
|
+
"SERVER",
|
|
130
|
+
"TIMEOUT",
|
|
131
|
+
"TRANSPORT"
|
|
132
|
+
]);
|
|
133
|
+
const backoffSchema = z.object({
|
|
134
|
+
initialDelayMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_INITIAL_DELAY_MS),
|
|
135
|
+
maxDelayMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_MAX_DELAY_MS),
|
|
136
|
+
jitterRatio: z.number().min(0).max(1).default(DEFAULT_JITTER_RATIO)
|
|
137
|
+
});
|
|
138
|
+
const normalPolicySchema = z.object({
|
|
139
|
+
mode: z.const("normal").required(),
|
|
140
|
+
maxRetries: z.number().step(1).min(0).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_RETRIES),
|
|
141
|
+
retryableCodes: z.array(z.string()).default([...DEFAULT_RETRYABLE_CODES]),
|
|
142
|
+
backoff: backoffSchema
|
|
143
|
+
});
|
|
144
|
+
const alwaysPolicySchema = z.object({
|
|
145
|
+
mode: z.const("always").required(),
|
|
146
|
+
backoff: backoffSchema
|
|
147
|
+
});
|
|
148
|
+
z.union([normalPolicySchema, alwaysPolicySchema]);
|
|
149
|
+
//#endregion
|
|
150
|
+
//#region ../../llm/llm/src/attribution.ts
|
|
151
|
+
/**
|
|
152
|
+
* Centralize the non-secret product identity every provider request sends as `User-Agent`, keeping
|
|
153
|
+
* adapters from drifting. See
|
|
154
|
+
* `.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md`.
|
|
155
|
+
*
|
|
156
|
+
* App-attribution vocabulary for provider requests.
|
|
157
|
+
* @module @deepseek-ai/dsh-llm/attribution
|
|
158
|
+
*/
|
|
159
|
+
const { version } = createRequire(import.meta.url)("../package.json");
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region lib/types/index.js
|
|
162
|
+
/**
|
|
163
|
+
* Opt-in request clock context. Eligible steps add durable,
|
|
164
|
+
* source-attributed time readings to the request history.
|
|
165
|
+
*
|
|
166
|
+
* @module @deepseek-ai/dsh-time-context
|
|
167
|
+
*/
|
|
168
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
169
|
+
const name = "time-context";
|
|
170
|
+
/** The agent registry that owns pre-step processing. */
|
|
171
|
+
const inject = ["agents"];
|
|
172
|
+
/** Schemastery validation for {@link Config}. */
|
|
173
|
+
const Config = z.object({
|
|
174
|
+
timeZone: z.string(),
|
|
175
|
+
refreshIntervalMs: z.number()
|
|
176
|
+
});
|
|
177
|
+
/** Format an epoch millisecond value as an ISO-shaped timestamp with offset and IANA zone. */
|
|
178
|
+
function formatTimestamp(now, formatter, timeZone) {
|
|
179
|
+
const parts = Object.fromEntries(formatter.formatToParts(now).map((part) => [part.type, part.value]));
|
|
180
|
+
const offset = parts.timeZoneName.replace(/^GMT$/, "GMT+00:00").slice(3);
|
|
181
|
+
return `${parts["year"]}-${parts["month"]}-${parts["day"]}T${parts["hour"]}:${parts["minute"]}:${parts["second"]}${offset}[${timeZone}]`;
|
|
182
|
+
}
|
|
183
|
+
/** Format a non-negative elapsed millisecond count as compact whole-second units. */
|
|
184
|
+
function formatDuration(elapsedMs) {
|
|
185
|
+
let seconds = Math.floor(Math.max(0, elapsedMs) / 1e3);
|
|
186
|
+
const days = Math.floor(seconds / 86400);
|
|
187
|
+
seconds %= 86400;
|
|
188
|
+
const hours = Math.floor(seconds / 3600);
|
|
189
|
+
seconds %= 3600;
|
|
190
|
+
const minutes = Math.floor(seconds / 60);
|
|
191
|
+
seconds %= 60;
|
|
192
|
+
const parts = [];
|
|
193
|
+
if (days > 0) parts.push(`${days}d`);
|
|
194
|
+
if (hours > 0) parts.push(`${hours}h`);
|
|
195
|
+
if (minutes > 0) parts.push(`${minutes}m`);
|
|
196
|
+
parts.push(`${seconds}s`);
|
|
197
|
+
return parts.join(" ");
|
|
198
|
+
}
|
|
199
|
+
/** Find the latest model-visible event, excluding this plugin's pending append. */
|
|
200
|
+
function precedingMessageTime(agent) {
|
|
201
|
+
for (const event of [...agent.session.events].reverse()) switch (event.type) {
|
|
202
|
+
case "user/message":
|
|
203
|
+
case "assistant/message":
|
|
204
|
+
case "tool/result": return event.time;
|
|
205
|
+
default: break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/** Find the preceding time-context event within the open turn. */
|
|
209
|
+
function precedingStepContextTime(agent, turn) {
|
|
210
|
+
for (const event of [...agent.session.events].reverse()) {
|
|
211
|
+
if (event.type === "turn/start" && event.data.turn === turn) return void 0;
|
|
212
|
+
if (event.type === "user/message" && event.data.source.kind === "plugin" && event.data.source.plugin === "time-context") return event.time;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** Find this plugin's latest durable injection, including a shadowed surface event. */
|
|
216
|
+
function latestInjectionTime(agent) {
|
|
217
|
+
for (const event of [...agent.session.events].reverse()) if (event.type === "user/message" && event.data.source.kind === "plugin" && event.data.source.plugin === "time-context") return event.time;
|
|
218
|
+
}
|
|
219
|
+
function renderText(now, turn, step, previous, formatter, timeZone) {
|
|
220
|
+
const elapsed = previous === void 0 ? "unavailable" : formatDuration(now - previous);
|
|
221
|
+
const baseline = step === 1 ? "model-visible message" : "step context";
|
|
222
|
+
return `Time sampled while preparing turn ${turn}, step ${step}: ${formatTimestamp(now, formatter, timeZone)}\nElapsed since the preceding ${baseline}: ${elapsed}.`;
|
|
223
|
+
}
|
|
224
|
+
/** Reject refresh intervals that cannot represent an exact elapsed-millisecond threshold. */
|
|
225
|
+
function validateRefreshInterval(refreshIntervalMs) {
|
|
226
|
+
if (refreshIntervalMs !== void 0 && (!Number.isSafeInteger(refreshIntervalMs) || refreshIntervalMs < 0)) throw new TypeError(`time-context: refreshIntervalMs must be a non-negative safe integer, got ${String(refreshIntervalMs)}`);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Register a prepended pre-step listener for the lifetime of `ctx`.
|
|
230
|
+
* @param ctx - plugin context; the listener is disposed with it.
|
|
231
|
+
* @param config - time zone and durable refresh scheduling configuration.
|
|
232
|
+
* @throws when the refresh interval is invalid or the configured or process time zone cannot be resolved.
|
|
233
|
+
*/
|
|
234
|
+
function apply(ctx, config) {
|
|
235
|
+
const timeZone = config.timeZone;
|
|
236
|
+
const refreshIntervalMs = config.refreshIntervalMs;
|
|
237
|
+
validateRefreshInterval(refreshIntervalMs);
|
|
238
|
+
let formatter;
|
|
239
|
+
try {
|
|
240
|
+
formatter = new Intl.DateTimeFormat("en-US", {
|
|
241
|
+
...timeZone === void 0 ? {} : { timeZone },
|
|
242
|
+
year: "numeric",
|
|
243
|
+
month: "2-digit",
|
|
244
|
+
day: "2-digit",
|
|
245
|
+
hour: "2-digit",
|
|
246
|
+
minute: "2-digit",
|
|
247
|
+
second: "2-digit",
|
|
248
|
+
hourCycle: "h23",
|
|
249
|
+
timeZoneName: "longOffset"
|
|
250
|
+
});
|
|
251
|
+
} catch (error) {
|
|
252
|
+
const message = timeZone === void 0 ? "time-context: failed to resolve the system time zone" : `time-context: invalid IANA timeZone ${JSON.stringify(timeZone)}`;
|
|
253
|
+
throw new Error(message, { cause: error });
|
|
254
|
+
}
|
|
255
|
+
const resolvedTimeZone = formatter.resolvedOptions().timeZone;
|
|
256
|
+
ctx.on("agent/pre-step", async ({ agent, turn, step, signal }, next) => {
|
|
257
|
+
const decision = await next();
|
|
258
|
+
if (decision.kind === "reject" || signal.aborted) return decision;
|
|
259
|
+
const now = Date.now();
|
|
260
|
+
if (refreshIntervalMs !== void 0 && refreshIntervalMs > 0) {
|
|
261
|
+
const lastInjection = latestInjectionTime(agent);
|
|
262
|
+
if (lastInjection !== void 0 && now >= lastInjection && now - lastInjection < refreshIntervalMs) return decision;
|
|
263
|
+
}
|
|
264
|
+
const text = renderText(now, turn, step, step === 1 ? precedingMessageTime(agent) : precedingStepContextTime(agent, turn), formatter, resolvedTimeZone);
|
|
265
|
+
return {
|
|
266
|
+
kind: "enter",
|
|
267
|
+
messages: [...decision.messages, createUserMessage({
|
|
268
|
+
content: [{
|
|
269
|
+
type: "text",
|
|
270
|
+
text
|
|
271
|
+
}],
|
|
272
|
+
source: {
|
|
273
|
+
kind: "plugin",
|
|
274
|
+
plugin: name,
|
|
275
|
+
form: "snapshot",
|
|
276
|
+
sections: [{
|
|
277
|
+
name,
|
|
278
|
+
text
|
|
279
|
+
}]
|
|
280
|
+
}
|
|
281
|
+
})]
|
|
282
|
+
};
|
|
283
|
+
}, { prepend: true });
|
|
284
|
+
}
|
|
285
|
+
//#endregion
|
|
286
|
+
export { Config, apply, inject, name };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/** Package-owned durable clock-context invariants. @module @deepseek-ai/dsh-time-context/invariant */
|
|
3
|
+
const PACKAGE_NAME = "@deepseek-ai/dsh-time-context";
|
|
4
|
+
const SOURCE_NAME = "time-context";
|
|
5
|
+
const READING = /* @__PURE__ */ new RegExp("^Time sampled while preparing turn (\\d+), step (\\d+): (\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:Z|[+-]\\d{2}:\\d{2})\\[[^\\]]+\\])\\nElapsed since the preceding (model-visible message|step context): (?:unavailable|(?:(?:\\d+d )?(?:\\d+h )?(?:\\d+m )?\\d+s))\\.$");
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
const name = "time-context-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
const inject = ["invariants"];
|
|
10
|
+
/** Derive the entered step boundary at which a time-context reading may append. */
|
|
11
|
+
function preparationPosition(history, fail) {
|
|
12
|
+
for (const event of history.slice().reverse()) switch (event.type) {
|
|
13
|
+
case "step/start": return {
|
|
14
|
+
turn: event.data.turn,
|
|
15
|
+
step: event.data.step
|
|
16
|
+
};
|
|
17
|
+
case "turn/start":
|
|
18
|
+
case "step/end":
|
|
19
|
+
case "turn/end":
|
|
20
|
+
case "request/header":
|
|
21
|
+
case "assistant/chunk":
|
|
22
|
+
case "assistant/message":
|
|
23
|
+
case "tool/call":
|
|
24
|
+
case "tool/result":
|
|
25
|
+
fail("time-context reading must be appended during prompt assembly");
|
|
26
|
+
break;
|
|
27
|
+
default: break;
|
|
28
|
+
}
|
|
29
|
+
fail("time-context reading must be appended during prompt assembly");
|
|
30
|
+
}
|
|
31
|
+
/** Validate one plugin-attributed time reading against its session position and timestamp. */
|
|
32
|
+
function validateReading(history, event, fail) {
|
|
33
|
+
const [block] = event.data.content;
|
|
34
|
+
if (event.data.content.length !== 1 || block?.type !== "text") fail("time-context messages must contain exactly one text block");
|
|
35
|
+
const match = READING.exec(block.text);
|
|
36
|
+
if (match === null) fail("time-context message does not match the durable reading format");
|
|
37
|
+
const turn = Number(match[1]);
|
|
38
|
+
const step = Number(match[2]);
|
|
39
|
+
if (!Number.isSafeInteger(turn) || turn < 1 || !Number.isSafeInteger(step) || step < 1) fail("time-context turn and step must be positive safe integers");
|
|
40
|
+
const expected = preparationPosition(history, fail);
|
|
41
|
+
if (turn !== expected.turn || step !== expected.step) fail(`time-context reading names turn ${turn}/step ${step}, expected turn ${expected.turn}/step ${expected.step}`);
|
|
42
|
+
const baseline = match[4];
|
|
43
|
+
if (step === 1 !== (baseline === "model-visible message")) fail(`time-context step ${step} uses the wrong elapsed-time baseline ${JSON.stringify(baseline)}`);
|
|
44
|
+
const rendered = match[3];
|
|
45
|
+
/* v8 ignore next -- the preceding fixed regexp always supplies capture group three. */
|
|
46
|
+
if (rendered === void 0) fail("time-context reading omitted its rendered timestamp");
|
|
47
|
+
const renderedTime = Date.parse(rendered.replace(/\[[^\]]+\]$/, ""));
|
|
48
|
+
if (!Number.isFinite(renderedTime) || !Number.isSafeInteger(event.time) || event.time < renderedTime) fail("time-context rendered timestamp must parse and not postdate its durable event");
|
|
49
|
+
}
|
|
50
|
+
/** Validate all package-owned readings already present in one session. */
|
|
51
|
+
function validateSession(session, fail) {
|
|
52
|
+
for (const [index, event] of session.events.entries()) {
|
|
53
|
+
if (event.type !== "user/message" || event.data.source.kind !== "plugin" || event.data.source.plugin !== SOURCE_NAME) continue;
|
|
54
|
+
validateReading(session.events.slice(0, index), event, fail);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Install validation for loaded and newly appended context readings. */
|
|
58
|
+
const install = Object.assign((ctx, fail) => {
|
|
59
|
+
for (const session of ctx.sessions.list()) validateSession(session, fail);
|
|
60
|
+
ctx.on("internal/dispatch", (_mode, eventName, args) => {
|
|
61
|
+
if (eventName !== "session/event") return;
|
|
62
|
+
const [session, event] = args;
|
|
63
|
+
if (event.type !== "user/message" || event.data.source.kind !== "plugin" || event.data.source.plugin !== SOURCE_NAME) return;
|
|
64
|
+
validateReading(session.events, event, fail);
|
|
65
|
+
}, { global: true });
|
|
66
|
+
}, { inject: ["sessions"] });
|
|
67
|
+
/**
|
|
68
|
+
* Register the time-context invariant companion.
|
|
69
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
70
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
71
|
+
*/
|
|
72
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
73
|
+
//#endregion
|
|
74
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in request clock context. Eligible steps add durable,
|
|
3
|
+
* source-attributed time readings to the request history.
|
|
4
|
+
*
|
|
5
|
+
* @module @deepseek-ai/dsh-time-context
|
|
6
|
+
*/
|
|
7
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
8
|
+
import z from '@deepseek-ai/schemastery';
|
|
9
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
10
|
+
export declare const name = "time-context";
|
|
11
|
+
/** The agent registry that owns pre-step processing. */
|
|
12
|
+
export declare const inject: string[];
|
|
13
|
+
/** Request-preparation clock formatting and append scheduling. Invalid values fail plugin load. */
|
|
14
|
+
export interface Config {
|
|
15
|
+
/** IANA time zone used for the rendered timestamp. Omit to resolve the Node process's system zone at plugin load. */
|
|
16
|
+
timeZone?: string;
|
|
17
|
+
/** Minimum milliseconds between durable injections in one session. Omit or set to 0 to inject at every eligible step. */
|
|
18
|
+
refreshIntervalMs?: number;
|
|
19
|
+
}
|
|
20
|
+
/** Schemastery validation for {@link Config}. */
|
|
21
|
+
export declare const Config: z<Config>;
|
|
22
|
+
/**
|
|
23
|
+
* Register a prepended pre-step listener for the lifetime of `ctx`.
|
|
24
|
+
* @param ctx - plugin context; the listener is disposed with it.
|
|
25
|
+
* @param config - time zone and durable refresh scheduling configuration.
|
|
26
|
+
* @throws when the refresh interval is invalid or the configured or process time zone cannot be resolved.
|
|
27
|
+
*/
|
|
28
|
+
export declare function apply(ctx: Context, config: Config): void;
|
|
29
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Package-owned durable clock-context invariants. @module @deepseek-ai/dsh-time-context/invariant */
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
/** Cordis companion plugin name. */
|
|
4
|
+
export declare const name = "time-context-invariant";
|
|
5
|
+
/** Service required before the companion can reserve package ownership. */
|
|
6
|
+
export declare const inject: string[];
|
|
7
|
+
/**
|
|
8
|
+
* Register the time-context invariant companion.
|
|
9
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
10
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
11
|
+
*/
|
|
12
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
13
|
+
//# sourceMappingURL=invariant.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deepseek-ai/dsh-time-context",
|
|
3
|
+
"description": "Opt-in durable per-step context with the current time and elapsed time",
|
|
4
|
+
"version": "0.0.1-rc.1",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "restricted"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/context/time-context"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./src/*": "./src/*",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib/index.js",
|
|
30
|
+
"lib/invariant.js",
|
|
31
|
+
"lib/types/**/*.d.ts"
|
|
32
|
+
],
|
|
33
|
+
"license": "BSD-3-Clause",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@deepseek-ai/schemastery": "^3.18.1-rc.1"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@deepseek-ai/dsh-agent": "^0.0.1-rc.1",
|
|
39
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
|
|
40
|
+
"@deepseek-ai/dsh-session": "^0.0.1-rc.1",
|
|
41
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@deepseek-ai/dsh-agent-loop": "^0.0.1-rc.1",
|
|
45
|
+
"@deepseek-ai/dsh-agent-loop-testkit": "^0.0.1-rc.1",
|
|
46
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
|
|
47
|
+
"@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
|
|
48
|
+
"@deepseek-ai/dsh-agent": "^0.0.1-rc.1",
|
|
49
|
+
"@deepseek-ai/dsh-loader-smoke": "^0.0.1-rc.1",
|
|
50
|
+
"@deepseek-ai/dsh-session": "^0.0.1-rc.1",
|
|
51
|
+
"@deepseek-ai/dsh-system-prompt": "^0.0.1-rc.1",
|
|
52
|
+
"@deepseek-ai/dsh-tools": "^0.0.1-rc.1",
|
|
53
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1"
|
|
54
|
+
}
|
|
55
|
+
}
|