From a9d74932b12e05721cdfb66c25d3aaf2ae70fde9 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:04:34 +0800 Subject: [PATCH] feat: add optional time context plugin --- AGENTS.md | 1 + docs/config-catalog.md | 16 + docs/module-graph.md | 6 + docs/rfc/INDEX.md | 1 + .../2026-07-14-time-context-plugin.i18n.yaml | 6 + .../feature/2026-07-14-time-context-plugin.md | 54 +++ .../2026-07-14-time-context-plugin.zh.md | 54 +++ packages/README.md | 3 +- packages/context/README.md | 7 + packages/context/time-context/README.md | 42 +++ packages/context/time-context/package.json | 41 ++ packages/context/time-context/src/index.ts | 189 ++++++++++ .../time-context/tests/time-context.spec.ts | 354 ++++++++++++++++++ packages/context/time-context/tsconfig.json | 15 + pnpm-lock.yaml | 28 ++ tsconfig.base.json | 1 + tsconfig.build.json | 1 + tsconfig.json | 1 + 18 files changed, 819 insertions(+), 1 deletion(-) create mode 100644 docs/rfc/implemented/feature/2026-07-14-time-context-plugin.i18n.yaml create mode 100644 docs/rfc/implemented/feature/2026-07-14-time-context-plugin.md create mode 100644 docs/rfc/implemented/feature/2026-07-14-time-context-plugin.zh.md create mode 100644 packages/context/README.md create mode 100644 packages/context/time-context/README.md create mode 100644 packages/context/time-context/package.json create mode 100644 packages/context/time-context/src/index.ts create mode 100644 packages/context/time-context/tests/time-context.spec.ts create mode 100644 packages/context/time-context/tsconfig.json diff --git a/AGENTS.md b/AGENTS.md index 4dfbedb97b..07cc20f706 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +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 subagent/ subagent seam + spawn/fork/ACP backends + delegation tool workflow/ workflow seam + worker-thread engine + the workflow tool todo/ the todo_write tool diff --git a/docs/config-catalog.md b/docs/config-catalog.md index f3765b9045..e07823c738 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -864,6 +864,22 @@ export interface Config { Source: [`packages/core/system-prompt/src/index.ts:227`](../packages/core/system-prompt/src/index.ts) +## `@deepseek-ai/dsh-time-context` + +Requires: `systemPrompt` + +```ts config-catalog +/** Configuration for the request-time clock section. */ +export interface Config { + /** IANA time zone used for the rendered timestamp (default `UTC`). */ + timeZone?: string + /** Maximum age of a reading within one turn, in milliseconds (default 60,000; `0` refreshes every step). */ + refreshIntervalMs?: number +} +``` + +Source: [`packages/context/time-context/src/index.ts:28`](../packages/context/time-context/src/index.ts) + ## `@deepseek-ai/dsh-tool-cordis` Requires: `tools` diff --git a/docs/module-graph.md b/docs/module-graph.md index 7d6bf65c21..19d471f175 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -107,6 +107,9 @@ flowchart TD pkg_code_runtime["code-runtime"] pkg_code_runtime_worker["code-runtime-worker"] end + subgraph group_context["packages/context"] + pkg_time_context["time-context"] + end subgraph group_guard["packages/guard"] pkg_repeat_tool_guard["repeat-tool-guard"] end @@ -183,6 +186,8 @@ flowchart TD pkg_user_approval --> pkg_system_prompt pkg_user_interaction --> pkg_agent pkg_user_interaction --> pkg_llm + pkg_time_context --> pkg_agent + pkg_time_context --> pkg_system_prompt pkg_workflow --> pkg_agent pkg_workflow --> pkg_brand pkg_workflow --> pkg_llm @@ -375,6 +380,7 @@ flowchart TD | [`invariants`](../packages/support/invariants) | `support` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session) | | [`user-approval`](../packages/ui/user-approval) | `ui` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) | | [`user-interaction`](../packages/ui/user-interaction) | `ui` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm) | +| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`system-prompt`](../packages/core/system-prompt) | | [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm) | | [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`code-runtime`](../packages/code-runtime/code-runtime), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/ui/user-approval) | | [`bash-sandbox`](../packages/bash/bash-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`bash-local`](../packages/bash/bash-local), [`sandbox`](../packages/sandbox/sandbox) | diff --git a/docs/rfc/INDEX.md b/docs/rfc/INDEX.md index c99226a264..b2268571d5 100644 --- a/docs/rfc/INDEX.md +++ b/docs/rfc/INDEX.md @@ -78,6 +78,7 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand; | [The self-referential cordis toolset](implemented/feature/2026-07-08-self-referential-cordis-toolset.md) | 2026-07-08 | | [Exact session query service](implemented/feature/2026-07-10-session-query-service.md) | 2026-07-10 | | [Configure subagent persona, tool visibility, and depth](implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md) | 2026-07-12 | +| [Optional time-context plugin](implemented/feature/2026-07-14-time-context-plugin.md) | 2026-07-14 | ### Simplification 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 new file mode 100644 index 0000000000..5ca24553a5 --- /dev/null +++ b/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# 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 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 new file mode 100644 index 0000000000..cab55c78c7 --- /dev/null +++ b/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.md @@ -0,0 +1,54 @@ +# RFC: Optional time-context plugin + +Status: implemented + +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. + +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. + +## 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. + +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. + +### 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)`. + +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. + +### 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. + +`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. + +### 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. + +## 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. + +## 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. + +## 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. 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 new file mode 100644 index 0000000000..a4644d8484 --- /dev/null +++ b/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.zh.md @@ -0,0 +1,54 @@ +# RFC:可选时间上下文插件 + +Status: implemented + +[English](2026-07-14-time-context-plugin.md) | 中文 + +## 问题 + +如果部署方既没有将当前时间硬编码到提示词中,也没有向模型提供查询时间的工具,agent(智能体)请求就无法获得实时准确的时钟信息。静态文本会立即失真,而对于日期、截止时间或会话闲置时长等常规推理,调用工具会带来不必要的开销。模型还缺少另一项配套信息:已经过去的时长。模型虽然能收到当前用户提示词,却无法区分紧接着发送的消息与上一条会话消息几小时后才发送的消息。 + +提示词组装流程和会话日志已经提供所需输入。区段提供方会在每个步骤中针对活跃 agent 运行一次;模型可见的会话事件带有持久的追加时间戳;请求头折叠结果会记录系统提示词实际渲染的确切内容。设计需要决定时间信息应归属何处、应以多高频率变化,同时避免累积陈旧读数或创建后台任务。 + +## 决策 + +`@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 处于空闲状态时,提示词组装结果中的该区段为空。 + +### 上一条消息基线 + +在轮次首次组装时,提供方从该轮次的 `turn/start` 向前扫描,采用最近的 `user/message`、`assistant/message`、`tool/result`、`context/message` 或 `steering/message` 时间戳。当前轮次中新追加的用户提示词会被刻意排除:若从该事件开始计时,首次请求会报告接近零的时长,从而丢失此功能要表达的轮次间隔。同一轮次内的后续刷新始终保留这条基线,因此长时间运行的轮次会报告从上一条会话消息起不断增加的时长。首个轮次报告 `unavailable (no earlier message in this session)`。 + +基线采用会话事件的追加时间,而不是日志中不存在的客户端接收时间。这样,恢复和 fork 行为都能从持久日志中确定性重现,模型可见值也无需新增事件即可重建。如果系统挂钟向后调整,插件会将显示时长钳制为零,而不会产生负数间隔。 + +### 刷新策略 + +`refreshIntervalMs` 默认值为 60,000,并且必须是非负安全整数。每个轮次的首次请求都强制刷新,不受上一轮次时间戳影响。在包含多个步骤的轮次内,后续组装会复用缓存区块,直至其存在时间达到该间隔;设为 `0` 时每个步骤都刷新。该策略仅由请求驱动:agent 正在等待模型调用、运行工具或处于空闲状态时,没有请求会消费新值,因此计时器不会创建任何任务。 + +`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)的所有权规则:可选插件拥有时间信息,并通过常规提示词注册表贡献该信息,不为循环添加特殊分支。 + +## 测试 + +该 package 的测试套件使用伪造的系统时间,覆盖 UTC 与偏移格式化、首轮次回退文本、所有符合条件的上一条消息类型、完整时长单位、挂钟回拨钳制、逐轮次刷新、间隔内复用、间隔到期、`0` 对应的逐步骤行为、相互独立的逐 agent 缓存、无效配置、HMR(热模块替换)资源释放以及 Loader 命名空间路径。一个使用真实 agent loop 的测试会固定实际发送的系统提示词及其 `request/header-delta` 刷新记录。默认快照不发生变化,因为所有仓库提供的组合都刻意不包含该插件;若在默认快照 fixture 中挂载它,将违反显式选择加入的决策。 + +## 考虑过的替代方案 + +- **每个轮次或每次刷新都追加一条 `context/message`**——不予采纳:每个读数都会留在派生历史中,因此陈旧时钟值和 token 成本会随会话长度累积。表层替换操作无法同时删除旧节点并将新读数移动到尾部;替换会保留旧节点的位置,而通过尾部节点替换又会隐藏中间的会话内容。 +- **使用 `agent/session-prefix`**——不予采纳:前缀在每个循环实例中只组装一次,并且按设计在会话期间保持稳定,因此无法表示每个轮次或步骤都会变化的时钟。 +- **在 `agent/request` 中修改请求**——不予采纳:该边界只负责塑造调用配置,触发时间晚于消息边界;如果在此处插入模型可见内容,会同时绕过提示词压力核算和请求头日志契约。 +- **注册独立的 `{{current_time}}` 和 `{{elapsed}}` 提示词变量**——不予采纳:两个独立提供方可能在不同时间点采样,并且需要共享缓存才能保证刷新语义的原子性。单个区段提供方将二者作为一个值计算和记录;部署方也不需要在角色设定中重复时间模板。 +- **按照配置的间隔通过后台计时器注入**——不予采纳:没有正在组装的模型请求时,新值没有消费方。由计时器驱动 `agent.inject()` 会创建持久的一次性轮次,并且只为通知时间流逝就唤醒或修改空闲会话。 +- **在 `dsh-agent-core` 中挂载插件**——不予采纳:时区、信息披露、token 预算和期望新鲜度都属于部署策略。显式选择加入能保持默认 harness 上下文稳定。 +- **将 package 放入 `core/`**——不予采纳:core 负责产品 API 主干。上下文增强是没有服务键的可选叶节点,因此专用分组能直接表达其组合角色。 + +## 后果 + +- 选择加入的部署无需消耗工具调用,即可让模型获得无歧义的分区时钟和轮次间隔时长。每个请求的系统提示词 token 成本固定,不会随会话增长。 +- 刷新会改变请求头,因此会新增一条 `request/header-delta` 事件。`refreshIntervalMs` 用时钟新鲜度换取这些持久增量记录的数量;将其设为零会在每个整秒渲染结果发生变化的步骤中刻意记录新值。 +- 系统不会仅为刷新时间而创建请求。工具运行时间超过该间隔时,先前读数会保持不变,直至下一步骤开始组装,此时提供方会追赶到当前时间。 +- 时长反映持久追加边界处的 harness 处理时间,不包含消息进入日志之前的客户端网络延迟。若要保留客户端来源时间戳,需要单独的持久输入契约,不属于本插件范围。 diff --git a/packages/README.md b/packages/README.md index 13da4b50da..63d17a5718 100644 --- a/packages/README.md +++ b/packages/README.md @@ -1,6 +1,6 @@ # Packages -Harness packages use the `@deepseek-ai/dsh-*` scope. Each is a Cordis plugin: a default `Service` subclass or functional plugin declaring ctx keys/events through declaration merging and contributing through `ctx.effect()`, `ctx.on()`, or `ctx.waterfall()`. Authoring conventions: [AGENTS.md](AGENTS.md) and root [AGENTS.md](../AGENTS.md) § Conventions. +Packages use the `@deepseek-ai/dsh-*` scope. Each is a Cordis `Service` subclass or function plugin; contributions use `ctx.effect()`, `ctx.on()`, or `ctx.waterfall()`. Authoring rules: [package](AGENTS.md) and [root](../AGENTS.md#conventions). ## Hierarchy @@ -16,6 +16,7 @@ Packages are grouped by modular role at `packages///`. The group dir | [`fs/`](fs/README.md) | Filesystem capability family: the abstract seam, a local impl, and the model-facing file tools | Product — stable surface | | [`skill/`](skill/README.md) | Skill capability family: the provider registry, local provider, and model-facing catalog/loader | Product — stable surface | | [`compact/`](compact/README.md) | Compaction capability family: the abstract seam + a basic backend (tool deferred) | Product — stable surface | +| [`context/`](context/README.md) | Opt-in request-context enrichment | Product — stable surface | | [`subagent/`](subagent/README.md) | Subagent capability family: the provider-registry seam and the model-facing delegation tool | Product — stable surface | | [`workflow/`](workflow/README.md) | Workflow capability family: the script-engine seam, the worker-thread engine, and the model-facing `workflow` tool | Product — stable surface | | [`web/`](web/README.md) | Web capability family: the abstract seam, search/fetch provider impls, and the model-facing web tools | Product — stable surface | diff --git a/packages/context/README.md b/packages/context/README.md new file mode 100644 index 0000000000..00755fce26 --- /dev/null +++ b/packages/context/README.md @@ -0,0 +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. + +| Package | Role | ctx key | +|---|---|---| +| `time-context/` | Dynamic current time and elapsed-since-previous-message system-prompt section | (none) | diff --git a/packages/context/time-context/README.md b/packages/context/time-context/README.md new file mode 100644 index 0000000000..3d9ffdd656 --- /dev/null +++ b/packages/context/time-context/README.md @@ -0,0 +1,42 @@ +# @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). + +## Config + +```yaml +- id: time-context + name: '@deepseek-ai/dsh-time-context' + config: + timeZone: UTC # default; any IANA time-zone identifier + 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. + +## 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 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. + +## 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. + +**Token effect**: Fixed two-line request context. A refresh replaces the section in the request header rather than retaining prior readings in conversation history. + +#### Temporal context section + +```markdown +Current time: +Time since previous message: . +``` + +## Known Limitations and Deferred Work + +- **Request-bound refresh only** — no clock update is emitted while the agent is waiting inside a model call or tool; the next assembled step refreshes once the configured interval has elapsed. +- **Whole-second display** — timestamps and durations omit sub-second precision even when `refreshIntervalMs` is below 1,000. +- **Session-event baseline** — elapsed time starts from the durable append timestamp, not a client transport's original send timestamp. diff --git a/packages/context/time-context/package.json b/packages/context/time-context/package.json new file mode 100644 index 0000000000..202976ccf8 --- /dev/null +++ b/packages/context/time-context/package.json @@ -0,0 +1,41 @@ +{ + "name": "@deepseek-ai/dsh-time-context", + "description": "Optional dynamic system-prompt context with the current time and elapsed duration since the previous message", + "version": "0.0.1", + "private": true, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./src/*": "./src/*", + "./package.json": "./package.json" + }, + "files": [ + "lib/index.js", + "lib/types/**/*.d.ts", + "lib/types/**/*.d.ts.map", + "src" + ], + "license": "BSD-3-Clause", + "dependencies": { + "schemastery": "^3.18.0" + }, + "peerDependencies": { + "@deepseek-ai/dsh-agent": "^0.0.1", + "@deepseek-ai/dsh-system-prompt": "^0.0.1", + "cordis": "^4.0.0-rc.6" + }, + "devDependencies": { + "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-agent-loop": "workspace:^", + "@deepseek-ai/dsh-llm": "workspace:^", + "@deepseek-ai/dsh-session": "workspace:^", + "@deepseek-ai/dsh-system-prompt": "workspace:^", + "@deepseek-ai/dsh-tools": "workspace:^", + "cordis": "^4.0.0-rc.6" + } +} diff --git a/packages/context/time-context/src/index.ts b/packages/context/time-context/src/index.ts new file mode 100644 index 0000000000..9162ef8614 --- /dev/null +++ b/packages/context/time-context/src/index.ts @@ -0,0 +1,189 @@ +/** + * 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. + * + * @module @deepseek-ai/dsh-time-context + */ + +import type { Context } from 'cordis' +import z from 'schemastery' +import type { Agent } from '@deepseek-ai/dsh-agent' +import type { AssembleContext } from '@deepseek-ai/dsh-system-prompt' + +/** Cordis plugin name used by loader diagnostics. */ +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. */ +export interface Config { + /** IANA time zone used for the rendered timestamp (default `UTC`). */ + timeZone?: string + /** Maximum age of a reading within one turn, in milliseconds (default 60,000; `0` refreshes every step). */ + refreshIntervalMs?: number +} + +/** Schemastery validation and defaults for {@link Config}. */ +export const Config: z = z.object({ + timeZone: z.string().default('UTC'), + 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. */ +interface RenderState { + turn: number + renderedAt: number + previousMessageTime: number | undefined + 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) { + case 'turn/end': + return undefined + case 'turn/start': + return { turn: event.data.turn, startSeq: event.seq } + default: + // Merge-extensible session events: only turn boundaries matter here. + break + } + } + return undefined +} + +/** Timestamp of the last model-visible message before one turn opened. */ +function previousMessageTime(agent: Agent, turnStartSeq: number): number | undefined { + for (const event of [...agent.session.events].reverse()) { + if (event.seq >= turnStartSeq) continue + switch (event.type) { + case 'user/message': + case 'assistant/message': + case 'tool/result': + case 'context/message': + case 'steering/message': + return event.time + default: + // Merge-extensible session events: non-surface records are not messages. + break + } + } + return undefined +} + +/** Format an epoch millisecond value as an ISO-shaped timestamp with offset and IANA zone. */ +function formatTimestamp(now: number, formatter: Intl.DateTimeFormat, timeZone: string): string { + const parts = Object.fromEntries( + formatter.formatToParts(now).map(part => [part.type, part.value]), + ) as Record + const offset = parts.timeZoneName.replace(/^GMT$/, 'GMT+00:00').slice(3) + return `${parts['year']}-${parts['month']}-${parts['day']}T${parts['hour']}:${parts['minute']}:${parts['second']}${offset}[${timeZone}]` +} + +/** Format a non-negative elapsed millisecond count as compact whole-second units. */ +function formatDuration(elapsedMs: number): string { + let seconds = Math.floor(Math.max(0, elapsedMs) / 1000) + const days = Math.floor(seconds / 86_400) + seconds %= 86_400 + const hours = Math.floor(seconds / 3600) + seconds %= 3600 + const minutes = Math.floor(seconds / 60) + seconds %= 60 + const parts: string[] = [] + if (days > 0) parts.push(`${days}d`) + if (hours > 0) parts.push(`${hours}h`) + if (minutes > 0) parts.push(`${minutes}m`) + parts.push(`${seconds}s`) + return parts.join(' ') +} + +/** Build the exact two-line model-facing section. */ +function renderText( + now: number, + previous: number | undefined, + formatter: Intl.DateTimeFormat, + timeZone: string, +): string { + const elapsed = previous === undefined + ? 'unavailable (no earlier message in this session)' + : formatDuration(now - previous) + return `Current time: ${formatTimestamp(now, formatter, timeZone)}\nTime since previous message: ${elapsed}.` +} + +/** + * Register the dynamic temporal system-prompt section. + * @param ctx - plugin context; the section registration is disposed with it. + * @param config - validated time zone and intra-turn refresh interval. + */ +export function apply(ctx: Context, config: Config): void { + const timeZone = config.timeZone as string + const refreshIntervalMs = config.refreshIntervalMs as number + if (!Number.isSafeInteger(refreshIntervalMs) || refreshIntervalMs < 0) { + throw new Error(`time-context: refreshIntervalMs must be a non-negative safe integer, got ${refreshIntervalMs}`) + } + + let formatter: Intl.DateTimeFormat + try { + formatter = new Intl.DateTimeFormat('en-US', { + timeZone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hourCycle: 'h23', + timeZoneName: 'longOffset', + }) + } catch (error: unknown) { + throw new Error(`time-context: invalid IANA timeZone ${JSON.stringify(timeZone)}`, { cause: error }) + } + const resolvedTimeZone = formatter.resolvedOptions().timeZone + const states = new WeakMap() + + ctx.systemPrompt.section({ + name: 'context:time', + order: 10, + text(context: AssembleContext): string { + const agent = context.agent + if (agent === undefined) return '' + const currentTurn = openTurn(agent) + if (currentTurn === undefined) return '' + + const now = Date.now() + const prior = states.get(agent) + if (prior !== undefined + && prior.turn === currentTurn.turn + && now >= prior.renderedAt + && now - prior.renderedAt < refreshIntervalMs) { + return prior.text + } + + const previous = prior?.turn === currentTurn.turn + ? prior.previousMessageTime + : previousMessageTime(agent, currentTurn.startSeq) + const text = renderText(now, previous, formatter, resolvedTimeZone) + states.set(agent, { turn: currentTurn.turn, renderedAt: now, previousMessageTime: previous, text }) + return text + }, + }) +} diff --git a/packages/context/time-context/tests/time-context.spec.ts b/packages/context/time-context/tests/time-context.spec.ts new file mode 100644 index 0000000000..c035a0594f --- /dev/null +++ b/packages/context/time-context/tests/time-context.spec.ts @@ -0,0 +1,354 @@ +/** 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' +import LlmService, { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm' +import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' +import SessionStore, { Session, SessionId, foldRequestHeader } from '@deepseek-ai/dsh-session' +import SystemPrompt from '@deepseek-ai/dsh-system-prompt' +import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent' +import type { Agent } from '@deepseek-ai/dsh-agent' +import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools' +import AgentLoop from '@deepseek-ai/dsh-agent-loop' +import * as timeContext from '@deepseek-ai/dsh-time-context' +import type { Config } from '@deepseek-ai/dsh-time-context' + +const BASE = Date.parse('2026-07-14T00:00:00.000Z') + +beforeEach(() => { + vi.useFakeTimers() + vi.setSystemTime(BASE) +}) + +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) + const fiber = await ctx.plugin(timeContext, 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', { + content: [{ type: 'text', text: `turn ${turn}` }], + source: { kind: 'user' }, + }, { surfaceOp: 'append' }) +} + +/** Script helper for a text-only model response. */ +function textResponse(text: string): StreamChunk[] { + return [ + { type: 'block-start', index: 0, blockType: 'text' }, + { type: 'block-end', index: 0, block: { type: 'text', text } }, + { type: 'finish', reason: { kind: 'stop' } }, + ] +} + +/** Script helper for one tool-call response. */ +function toolCallResponse(): StreamChunk[] { + return [ + { type: 'block-start', index: 0, blockType: 'tool-call' }, + { + type: 'block-end', + index: 0, + block: { type: 'tool-call', id: CallId('tick-1'), name: 'tick', arguments: '{}' }, + }, + { type: 'finish', reason: { kind: 'tool-calls' } }, + ] +} + +/** Deterministic adapter that records each request and consumes one chunk script. */ +class ScriptedAdapter extends LlmAdapter { + readonly requests: GenerateOptions[] = [] + + constructor(private readonly script: StreamChunk[][]) { + super() + } + + override async * stream(options: GenerateOptions): AsyncIterable { + this.requests.push(options) + const chunks = this.script.shift() + if (chunks === undefined) throw new Error('ScriptedAdapter: script exhausted') + for (const chunk of chunks) yield chunk + } +} + +/** 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) + await ctx.plugin(SessionStore) + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRegistry) + await ctx.plugin(AgentRegistry) + await ctx.plugin(AgentLoop, { agents: [] }) + await ctx.plugin(timeContext, config) + ctx.llm.registerAdapter(['mock'], adapter) + return ctx +} + +describe('temporal section rendering', () => { + it('renders the first turn in UTC with the explicit no-previous-message fallback', async () => { + const { ctx } = await mount() + const session = new Session(SessionId('first')) + openMessageTurn(session, 1) + + expect(await sectionText(ctx, sessionAgent(session))).toBe( + 'Current time: 2026-07-14T00:00:00+00:00[UTC]\n' + + 'Time since previous message: unavailable (no earlier message in this session).', + ) + }) + + it('renders a non-UTC numeric offset and all compact duration units', async () => { + const { ctx } = await mount({ timeZone: 'Asia/Shanghai' }) + const session = new Session(SessionId('offset')) + session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) + session.append('assistant/message', { + turn: 1, + step: 1, + content: [{ type: 'text', text: 'previous' }], + }, { surfaceOp: 'append' }) + session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) + vi.setSystemTime(BASE + 90_061_000) + openMessageTurn(session, 2) + + expect(await sectionText(ctx, sessionAgent(session))).toBe( + 'Current time: 2026-07-15T09:01:01+08:00[Asia/Shanghai]\n' + + 'Time since previous message: 1d 1h 1m 1s.', + ) + }) + + it('clamps a backward wall-clock adjustment to a zero duration', async () => { + const { ctx } = await mount() + const session = new Session(SessionId('backward-duration')) + session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) + session.append('assistant/message', { + turn: 1, + step: 1, + content: [{ type: 'text', text: 'future by adjusted clock' }], + }, { surfaceOp: 'append' }) + session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) + vi.setSystemTime(BASE - 5_000) + openMessageTurn(session, 2) + + expect(await sectionText(ctx, sessionAgent(session))).toContain('Time since previous message: 0s.') + }) + + const previousMessageCases = [ + ['user/message', (session: Session): void => { + session.append('user/message', { content: [{ type: 'text', text: 'u' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) + }], + ['assistant/message', (session: Session): void => { + session.append('assistant/message', { turn: 1, step: 1, content: [{ type: 'text', text: 'a' }] }, { surfaceOp: 'append' }) + }], + ['tool/result', (session: Session): void => { + session.append('tool/result', { + turn: 1, + step: 1, + callId: CallId('previous'), + content: [{ type: 'text', text: 'r' }], + isError: false, + }, { surfaceOp: 'append' }) + }], + ['context/message', (session: Session): void => { + session.append('context/message', { + content: [{ type: 'text', text: 'c' }], + source: { kind: 'plugin', plugin: 'test' }, + }, { surfaceOp: 'append' }) + }], + ['steering/message', (session: Session): void => { + session.append('steering/message', { + turn: 1, + content: [{ type: 'text', text: 's' }], + source: { kind: 'user' }, + }, { surfaceOp: 'append' }) + }], + ] as const + + it.each(previousMessageCases)('uses a prior %s as the duration baseline', async (_name, appendPrevious) => { + const { ctx } = await mount() + const session = new Session(SessionId(`previous-${_name}`)) + session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) + appendPrevious(session) + session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) + vi.setSystemTime(BASE + 5_000) + openMessageTurn(session, 2) + + expect(await sectionText(ctx, sessionAgent(session))).toContain('Time since previous message: 5s.') + }) + + it('contributes empty text without an active agent turn', async () => { + const { ctx } = await mount() + expect(await sectionText(ctx)).toBe('') + + const empty = sessionAgent(new Session(SessionId('empty'))) + expect(await sectionText(ctx, empty)).toBe('') + + const closedSession = new Session(SessionId('closed')) + openMessageTurn(closedSession, 1) + closedSession.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) + expect(await sectionText(ctx, sessionAgent(closedSession))).toBe('') + }) +}) + +describe('refresh policy', () => { + it('reuses within the interval, refreshes at expiry, and refreshes after a backward clock jump', async () => { + const { ctx } = await mount({ refreshIntervalMs: 60_000 }) + const session = new Session(SessionId('interval')) + const agent = sessionAgent(session) + openMessageTurn(session, 1) + + const first = await sectionText(ctx, agent) + vi.setSystemTime(BASE + 30_000) + expect(await sectionText(ctx, agent)).toBe(first) + vi.setSystemTime(BASE + 60_000) + const expired = await sectionText(ctx, agent) + expect(expired).toContain('2026-07-14T00:01:00+00:00[UTC]') + vi.setSystemTime(BASE + 59_000) + expect(await sectionText(ctx, agent)).toContain('2026-07-14T00:00:59+00:00[UTC]') + }) + + it('refreshes every assembly when refreshIntervalMs is zero', async () => { + const { ctx } = await mount({ refreshIntervalMs: 0 }) + const session = new Session(SessionId('every-step')) + const agent = sessionAgent(session) + openMessageTurn(session, 1) + const first = await sectionText(ctx, agent) + vi.setSystemTime(BASE + 1_000) + expect(await sectionText(ctx, agent)).not.toBe(first) + }) + + it('always refreshes for a new turn and keeps the preceding message baseline', async () => { + const { ctx } = await mount({ refreshIntervalMs: 60_000 }) + const session = new Session(SessionId('turn-refresh')) + const agent = sessionAgent(session) + openMessageTurn(session, 1) + const first = await sectionText(ctx, agent) + vi.setSystemTime(BASE + 1_000) + session.append('assistant/message', { + turn: 1, + step: 1, + content: [{ type: 'text', text: 'done' }], + }, { surfaceOp: 'append' }) + session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) + vi.setSystemTime(BASE + 2_000) + openMessageTurn(session, 2) + + const second = await sectionText(ctx, agent) + expect(second).not.toBe(first) + expect(second).toContain('Time since previous message: 1s.') + }) + + it('keeps refresh caches independent per agent', async () => { + const { ctx } = await mount({ refreshIntervalMs: 60_000 }) + const sessionA = new Session(SessionId('agent-a')) + const sessionB = new Session(SessionId('agent-b')) + const agentA = sessionAgent(sessionA, 'a') + const agentB = sessionAgent(sessionB, 'b') + openMessageTurn(sessionA, 1) + openMessageTurn(sessionB, 1) + const aFirst = await sectionText(ctx, agentA) + vi.setSystemTime(BASE + 30_000) + const bFirst = await sectionText(ctx, agentB) + vi.setSystemTime(BASE + 40_000) + + expect(await sectionText(ctx, agentA)).toBe(aFirst) + expect(bFirst).toContain('2026-07-14T00:00:30+00:00[UTC]') + }) +}) + +describe('configuration and lifecycle', () => { + it('fails loud for negative, fractional, unsafe, and invalid-zone config', async () => { + for (const refreshIntervalMs of [-1, 1.5, Number.MAX_SAFE_INTEGER + 1]) { + const ctx = new Context() + await ctx.plugin(SystemPrompt) + await expect(ctx.plugin(timeContext, { refreshIntervalMs })).rejects.toThrow(/non-negative safe integer/) + } + + const ctx = new Context() + await ctx.plugin(SystemPrompt) + await expect(ctx.plugin(timeContext, { timeZone: 'Not/A_Real_Zone' })).rejects.toThrow(/invalid IANA timeZone/) + }) + + it('removes its section when the plugin fiber disposes', async () => { + const { ctx, fiber } = await mount() + const session = new Session(SessionId('dispose')) + const agent = sessionAgent(session) + openMessageTurn(session, 1) + expect(await sectionText(ctx, agent)).toContain('Current time:') + + await fiber.dispose() + expect(await sectionText(ctx, agent)).toBeUndefined() + }) +}) + +describe('real agent-loop request logging', () => { + it('refreshes a long turn in the system prompt and records the header delta without context history', async () => { + const adapter = new ScriptedAdapter([toolCallResponse(), textResponse('done'), textResponse('next turn')]) + const ctx = await loopHarness(adapter, { refreshIntervalMs: 60_000 }) + ctx.tools.register(defineTool({ + name: 'tick', + description: 'advance fake time', + parameters: {}, + async execute() { + vi.setSystemTime(BASE + 61_000) + return [{ type: 'text' as const, text: 'advanced' }] + }, + })) + const agent = ctx.agentLoop.create(AgentId('loop'), { model: 'mock' }) + + agent.send([{ type: 'text', text: 'start' }]) + await agent.whenIdle() + expect(adapter.requests).toHaveLength(2) + expect(adapter.requests[0]!.system).toContain('2026-07-14T00:00:00+00:00[UTC]') + expect(adapter.requests[1]!.system).toContain('2026-07-14T00:01:01+00:00[UTC]') + expect(agent.session.events.some(event => event.type === 'context/message')).toBe(false) + expect(agent.session.events.filter(event => event.type === 'request/header-delta')).toHaveLength(1) + expect(foldRequestHeader(agent.session.events)?.system).toBe(adapter.requests[1]!.system) + + vi.setSystemTime(BASE + 361_000) + agent.send([{ type: 'text', text: 'again' }]) + await agent.whenIdle() + expect(adapter.requests[2]!.system).toContain('Time since previous message: 5m 0s.') + await ctx.fiber.dispose() + }) +}) + +describe('real Loader export path', () => { + it('keeps the namespace metadata and boots through unwrapExports', async () => { + expect('default' in timeContext).toBe(false) + const loader = Object.create(Loader.prototype) as Loader + const unwrapped = loader.unwrapExports(timeContext) as Record + expect(unwrapped).toBe(timeContext) + expect(unwrapped.name).toBe('time-context') + expect(unwrapped.inject).toEqual(['systemPrompt']) + expect(unwrapped.Config).toBeDefined() + expect(typeof unwrapped.apply).toBe('function') + + const ctx = new Context() + await ctx.plugin(SystemPrompt) + const plugin = loader.unwrapExports(timeContext) as Parameters[0] + await ctx.plugin(plugin) + const session = new Session(SessionId('loader')) + openMessageTurn(session, 1) + expect(await sectionText(ctx, sessionAgent(session))).toContain('Current time:') + }) +}) diff --git a/packages/context/time-context/tsconfig.json b/packages/context/time-context/tsconfig.json new file mode 100644 index 0000000000..eda3a81772 --- /dev/null +++ b/packages/context/time-context/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": ["src"], + "references": [ + { "path": "../../../vendor/cosmokit" }, + { "path": "../../../vendor/cordis" }, + { "path": "../../../vendor/schemastery" }, + { "path": "../../core/system-prompt" }, + { "path": "../../core/agent" } + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6b0d4519c..650addb80a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -240,6 +240,34 @@ importers: specifier: ^4.0.0-rc.6 version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + packages/context/time-context: + dependencies: + schemastery: + specifier: ^3.18.0 + version: 3.18.0 + devDependencies: + '@deepseek-ai/dsh-agent': + specifier: workspace:^ + version: link:../../core/agent + '@deepseek-ai/dsh-agent-loop': + specifier: workspace:^ + version: link:../../core/agent-loop + '@deepseek-ai/dsh-llm': + specifier: workspace:^ + version: link:../../llm/llm + '@deepseek-ai/dsh-session': + specifier: workspace:^ + version: link:../../core/session + '@deepseek-ai/dsh-system-prompt': + specifier: workspace:^ + version: link:../../core/system-prompt + '@deepseek-ai/dsh-tools': + specifier: workspace:^ + version: link:../../core/tools + cordis: + specifier: ^4.0.0-rc.6 + version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + packages/cordis/tool-cordis: dependencies: schemastery: diff --git a/tsconfig.base.json b/tsconfig.base.json index c6dff7c732..29f954f68f 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -47,6 +47,7 @@ "./packages/fs/*/src", "./packages/skill/*/src", "./packages/compact/*/src", + "./packages/context/*/src", "./packages/guard/*/src", "./packages/subagent/*/src", "./packages/workflow/*/src", diff --git a/tsconfig.build.json b/tsconfig.build.json index 6deb2c6e01..591955b260 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -21,6 +21,7 @@ { "path": "./packages/session-query/session-query" }, { "path": "./packages/core/system-prompt" }, { "path": "./packages/core/agent" }, + { "path": "./packages/context/time-context" }, { "path": "./packages/ui/user-interaction" }, { "path": "./packages/ui/user-approval" }, { "path": "./packages/ui/permission" }, diff --git a/tsconfig.json b/tsconfig.json index dd283ec5d7..e97a8295a5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,6 +32,7 @@ { "path": "./packages/session-query/session-query" }, { "path": "./packages/core/system-prompt" }, { "path": "./packages/core/agent" }, + { "path": "./packages/context/time-context" }, { "path": "./packages/ui/user-interaction" }, { "path": "./packages/ui/user-approval" }, { "path": "./packages/ui/permission" },