docs: align time-context prose standard
This commit is contained in:
@@ -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) |
|
||||
|
||||
@@ -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. `<timestamp>` is an ISO-shaped local timestamp with numeric offset and IANA zone; `<duration-or-unavailable>` 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. `<timestamp>` is an ISO-shaped local timestamp with numeric offset and IANA zone; `<duration-or-unavailable>` 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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<Config> = 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
|
||||
|
||||
@@ -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<string | undefined> {
|
||||
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<Context> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
|
||||
Reference in New Issue
Block a user