refactor agent pre-step inbox lifecycle

This commit is contained in:
_Kerman
2026-07-31 19:21:16 +08:00
parent c2ff9ddec8
commit fcc2b5e282
267 changed files with 2052 additions and 1546 deletions

View File

@@ -1,26 +1,26 @@
/**
* Opt-in request-preparation clock context. Eligible pre-step attempts append
* durable, source-attributed time readings to conversation history.
* Opt-in request clock context. Eligible steps add durable,
* source-attributed time readings to the request history.
*
* @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 { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
/** Cordis plugin name used by loader diagnostics. */
export const name = 'time-context'
/** The agent registry that owns the pre-step lifecycle seam. */
/** The agent registry that owns pre-step processing. */
export const inject = ['agents']
/** Request-preparation clock formatting and append scheduling. Invalid values fail plugin load. */
export interface Config {
/** IANA time zone used for the rendered timestamp. Omit to resolve the Node process's system zone at plugin load. */
timeZone?: string
/** Minimum milliseconds between durable injections in one session. Omit or set to 0 to inject on every eligible pre-step attempt. */
/** Minimum milliseconds between durable injections in one session. Omit or set to 0 to inject at every eligible step. */
refreshIntervalMs?: number
}
@@ -157,23 +157,33 @@ export function apply(ctx: Context, config: Config): void {
}
const resolvedTimeZone = formatter.resolvedOptions().timeZone
ctx.on('agent/step', (
ctx.on('agent/pre-step', async (
agent: Agent,
turn: number,
step: number,
signal: AbortSignal,
) => {
if (signal.aborted) return
_messages,
{ turn, step, signal },
next,
): Promise<PreStepDecision> => {
const decision = await next()
if (decision.kind === 'reject' || signal.aborted) return decision
const now = Date.now()
if (refreshIntervalMs !== undefined && refreshIntervalMs > 0) {
const lastInjection = latestInjectionTime(agent)
if (lastInjection !== undefined
&& now >= lastInjection
&& now - lastInjection < refreshIntervalMs) return
&& now - lastInjection < refreshIntervalMs) return decision
}
const previous = step === 1
? precedingMessageTime(agent)
: precedingStepContextTime(agent, turn)
agent.session.append('user/message', createUserMessage({ content: [{ type: 'text', text: renderText(now, turn, step, previous, formatter, resolvedTimeZone) }], source: { kind: 'plugin', plugin: name } }), { surfaceOp: 'append' })
return {
kind: 'enter',
messages: [
...decision.messages,
createUserMessage({
content: [{ type: 'text', text: renderText(now, turn, step, previous, formatter, resolvedTimeZone) }],
source: { kind: 'plugin', plugin: name },
}),
],
}
}, { prepend: true })
}

View File

@@ -18,22 +18,23 @@ export const name = 'time-context-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/** Derive the open step in which a time-context reading may append. */
/** Derive the next request boundary at which a time-context reading may append. */
function preparationPosition(history: readonly SessionEvent[], fail: InvariantFailure): { turn: number; step: number } {
for (const event of history.slice().reverse()) {
switch (event.type) {
case 'step/start':
return event.data
case 'step/end':
return { turn: event.data.turn, step: event.data.step + 1 }
case 'turn/start':
return { turn: event.data.turn, step: 1 }
case 'step/start':
case 'turn/end':
fail('time-context reading must be appended inside an open step')
fail('time-context reading must be appended at a prompt boundary')
break
default:
break
}
}
fail('time-context reading must be appended inside an open step')
fail('time-context reading must be appended at a prompt boundary')
}
/** Validate one plugin-attributed time reading against its session position and timestamp. */