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

View File

@@ -8,9 +8,9 @@
*/
import { describe, expect, it } from 'vitest'
import { Session, SessionId, applyHeaderDelta, canonicalHeader, diffHeader, foldRequestHeader } from '@deepseek-ai/dsh-session'
import { Session, SessionId, applyHeaderDelta, canonicalHeader, diffHeader, foldRequestHeader, headerEquals } from '@deepseek-ai/dsh-session'
import type { EpochHeader, SessionEvent } from '@deepseek-ai/dsh-session'
import type { ToolSchema } from '@deepseek-ai/dsh-llm'
import type { Message, ToolSchema } from '@deepseek-ai/dsh-llm'
const CONFIG = { model: 'm' }
@@ -18,6 +18,10 @@ function tool(name: string, description = 'd'): ToolSchema {
return { name, description, parameters: { type: 'object' } }
}
function msg(text: string): Message {
return { role: 'user', content: [{ type: 'text', text }] }
}
/** Round-trip helper: diff must reproduce `next` from `prev` exactly. */
function roundTrip(prev: EpochHeader, next: EpochHeader): ReturnType<typeof diffHeader> {
const delta = diffHeader(prev, next)
@@ -103,6 +107,49 @@ describe('diffHeader / applyHeaderDelta', () => {
})
})
describe('request-only messages (messagePrefix / messageSuffix)', () => {
it('canonicalHeader normalizes empty arrays to absent fields', () => {
expect(canonicalHeader({ config: CONFIG, messagePrefix: [], messageSuffix: [] })).toEqual({ config: CONFIG })
const full = canonicalHeader({ config: CONFIG, messagePrefix: [msg('p')], messageSuffix: [msg('s')] })
expect(full.messagePrefix).toEqual([msg('p')])
expect(full.messageSuffix).toEqual([msg('s')])
})
it('headerEquals treats absence and empty as one representation, content differences as unequal', () => {
expect(headerEquals(canonicalHeader({ config: CONFIG }), { config: CONFIG, messagePrefix: [] })).toBe(true)
expect(headerEquals({ config: CONFIG, messagePrefix: [msg('a')] }, { config: CONFIG, messagePrefix: [msg('b')] })).toBe(false)
expect(headerEquals({ config: CONFIG, messageSuffix: [msg('a')] }, { config: CONFIG })).toBe(false)
})
it('replaces a changed prefix whole and leaves an untouched suffix alone', () => {
const prev = canonicalHeader({ config: CONFIG, messagePrefix: [msg('old')], messageSuffix: [msg('keep')] })
const next = canonicalHeader({ config: CONFIG, messagePrefix: [msg('new'), msg('more')], messageSuffix: [msg('keep')] })
const delta = roundTrip(prev, next)
expect(delta).toEqual({ messagePrefix: [msg('new'), msg('more')] })
})
it('round-trips framing gained from a bare header and lost back to one (empty array encodes absence)', () => {
const none = canonicalHeader({ config: CONFIG })
const some = canonicalHeader({ config: CONFIG, messagePrefix: [msg('p')], messageSuffix: [msg('s')] })
const gained = roundTrip(none, some)
expect(gained).toEqual({ messagePrefix: [msg('p')], messageSuffix: [msg('s')] })
const lost = roundTrip(some, none)
expect(lost).toEqual({ messagePrefix: [], messageSuffix: [] })
})
it('folds framing deltas over the log like any other header amendment', () => {
const session = new Session(SessionId('fold-framing'))
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
const first = canonicalHeader({ config: CONFIG, messagePrefix: [msg('catalog v1')] })
session.append('request/header', { header: first, reason: 'initial' })
const second = canonicalHeader({ config: CONFIG, messagePrefix: [msg('catalog v2')] })
session.append('request/header-delta', diffHeader(first, second)!)
expect(foldRequestHeader(session.events)).toEqual(second)
session.append('request/header-delta', diffHeader(second, canonicalHeader({ config: CONFIG }))!)
expect(foldRequestHeader(session.events)).toEqual({ config: CONFIG })
})
})
describe('foldRequestHeader', () => {
function headerEvents(session: Session): readonly SessionEvent[] {
return session.events