feat(agent): add the agent/request-messages request-only message seam

A new waterfall near request construction lets plugins contribute
request-ONLY messages framing the derived history: RequestMessages
{ before, after } with a frozen empty seed, fired inside the open step
after the agent/request config waterfall, so the step/start boundary
snapshot and its same-sync-frame invariant are untouched. The request
becomes messagePrefix + boundary snapshot + messageSuffix.

Contributions never enter session history — deriveMessages() is
unchanged — so the request header is their durable record:
EpochHeader gains messagePrefix/messageSuffix (canonical absence for
empty arrays), request/header-delta replaces either array whole with
an empty array encoding the transition back to absence, and the
dev-mode reconstruction cross-check now expects the folded header's
framing around the boundary derivation.

This is the seam for per-request advisory context that must be
model-visible now without becoming durable history (a skills catalog,
an environment reminder), keeping the base system prompt
workspace-independent and provider prefix caches stable. The docs
carry the channel cost model: session-frozen content belongs in
before, low-frequency change notices belong in durable history via
inject() (paid once, prefix-cached thereafter), and after is reserved
for small frequently-refreshed state snapshots re-paid on every
request they ride. No shipped producer yet, so ACP snapshot fixtures
are byte-identical.
This commit is contained in:
Yichen Jiang
2026-07-07 19:42:30 +08:00
parent e477d76199
commit 17bd71e530
20 changed files with 553 additions and 119 deletions
+38 -14
View File
@@ -13,14 +13,24 @@
*/
import { callConfigEquals } from '@deepseek-ai/dsh-llm'
import type { LlmCallConfig, ToolSchema } from '@deepseek-ai/dsh-llm'
import type { LlmCallConfig, Message, ToolSchema } from '@deepseek-ai/dsh-llm'
import type { EpochHeader, SessionEvent, SystemDelta, ToolsDelta } from './types.ts'
/** The `request/header-delta` payload shape: each present field amends the folded header. */
type HeaderDelta = {
system?: SystemDelta
tools?: ToolsDelta
config?: LlmCallConfig
messagePrefix?: Message[]
messageSuffix?: Message[]
}
/**
* Normalize a header to canonical form: an empty system prompt and an empty
* tool list become ABSENT fields, matching how requests are built (both
* request-build spreads skip empty values). Diff, fold, and comparison all
* operate on canonical headers, so "no system prompt" has exactly one
* Normalize a header to canonical form: an empty system prompt, an empty
* tool list, and empty request-only message arrays become ABSENT fields,
* matching how requests are built (the request-build spreads skip empty
* values). Diff, fold, and comparison all operate on canonical headers, so
* "no system prompt" (and "no request-only messages") has exactly one
* representation.
* @param header - the header to normalize (not mutated).
* @returns the canonical header.
@@ -30,6 +40,8 @@ export function canonicalHeader(header: EpochHeader): EpochHeader {
config: header.config,
...header.system !== undefined && header.system.length > 0 ? { system: header.system } : {},
...header.tools !== undefined && header.tools.length > 0 ? { tools: header.tools } : {},
...header.messagePrefix !== undefined && header.messagePrefix.length > 0 ? { messagePrefix: header.messagePrefix } : {},
...header.messageSuffix !== undefined && header.messageSuffix.length > 0 ? { messageSuffix: header.messageSuffix } : {},
}
}
@@ -109,37 +121,47 @@ function applyTools(prev: readonly ToolSchema[], delta: ToolsDelta): ToolSchema[
* writer's round-trip guard runs (`applyHeaderDelta(prev, delta)` must equal
* the intended header) and the loop runs to skip logging an unchanged header.
* Tools compare per-schema IN ORDER (canonical JSON), so a pure reordering is
* correctly unequal.
* correctly unequal; request-only message arrays compare as canonical JSON
* (both sides come from the same build path, so key order matches when the
* values do).
* @param a - one canonical header.
* @param b - the other.
* @returns whether config, system, and tools (in order) all match.
* @returns whether config, system, tools (in order), and request-only messages all match.
*/
export function headerEquals(a: EpochHeader, b: EpochHeader): boolean {
if (!callConfigEquals(a.config, b.config) || a.system !== b.system) return false
if (!sameMessages(a.messagePrefix, b.messagePrefix) || !sameMessages(a.messageSuffix, b.messageSuffix)) return false
const at = a.tools ?? []
const bt = b.tools ?? []
return at.length === bt.length && at.every((tool, i) => sameSchema(tool, bt[i] as ToolSchema))
}
/** Canonical JSON equality over request-only message arrays; absence equals the empty array. */
function sameMessages(a: readonly Message[] | undefined, b: readonly Message[] | undefined): boolean {
return JSON.stringify(a ?? []) === JSON.stringify(b ?? [])
}
/**
* Compute the `request/header-delta` payload between two canonical headers,
* or undefined when they are equal. The caller MUST round-trip the result
* ({@link applyHeaderDelta} on `prev` deep-equals `next`) before logging it —
* the encoding cannot express every change (a pure tool reordering) — and
* fall back to a full `request/header` snapshot when the check fails.
* Request-only messages are replaced whole (small advisory content, not worth
* diffing); an empty replacement array encodes the transition to "none".
* @param prev - the folded header the log currently implies.
* @param next - the header the next request will actually use.
* @returns the delta payload, or undefined when nothing changed.
*/
export function diffHeader(
prev: EpochHeader, next: EpochHeader,
): { system?: SystemDelta; tools?: ToolsDelta; config?: LlmCallConfig } | undefined {
const delta: { system?: SystemDelta; tools?: ToolsDelta; config?: LlmCallConfig } = {}
export function diffHeader(prev: EpochHeader, next: EpochHeader): HeaderDelta | undefined {
const delta: HeaderDelta = {}
if (prev.system !== next.system) delta.system = diffSystem(prev.system, next.system)
const prevTools = prev.tools ?? []
const nextTools = next.tools ?? []
if (JSON.stringify(prevTools) !== JSON.stringify(nextTools)) delta.tools = diffTools(prevTools, nextTools)
if (!callConfigEquals(prev.config, next.config)) delta.config = next.config
if (!sameMessages(prev.messagePrefix, next.messagePrefix)) delta.messagePrefix = next.messagePrefix ?? []
if (!sameMessages(prev.messageSuffix, next.messageSuffix)) delta.messageSuffix = next.messageSuffix ?? []
return Object.keys(delta).length > 0 ? delta : undefined
}
@@ -151,15 +173,17 @@ export function diffHeader(
* @param delta - the logged delta payload.
* @returns the canonical header after the delta.
*/
export function applyHeaderDelta(
prev: EpochHeader, delta: { system?: SystemDelta; tools?: ToolsDelta; config?: LlmCallConfig },
): EpochHeader {
export function applyHeaderDelta(prev: EpochHeader, delta: HeaderDelta): EpochHeader {
const system = delta.system !== undefined ? applySystem(prev.system, delta.system) : prev.system
const tools = delta.tools !== undefined ? applyTools(prev.tools ?? [], delta.tools) : prev.tools
const messagePrefix = delta.messagePrefix ?? prev.messagePrefix
const messageSuffix = delta.messageSuffix ?? prev.messageSuffix
return canonicalHeader({
config: delta.config ?? prev.config,
...system !== undefined ? { system } : {},
...tools !== undefined ? { tools } : {},
...messagePrefix !== undefined ? { messagePrefix } : {},
...messageSuffix !== undefined ? { messageSuffix } : {},
})
}