refactor(agent): rename agent/request-messages to agent/request-advice
tianyicui's review: the seam name did not say what the event or its types do. 'advice' reads both ways — advisory content for the model, and AOP before/after advice woven around a join point (here the derived history) without modifying it — so RequestAdvice.before/after are self-describing. Types follow: RequestAdvice / RequestAdviceContext; the logged EpochHeader fields keep their positional names (messagePrefix/messageSuffix). Also sharpens the core.md wording the review flagged as ambiguous: before-advice sits in front of the ENTIRE derived history, directly after the system slot (the conventional home for session-stable openers — an AGENTS.md digest, a skills catalog), after-advice follows the history's last message. Catalogs and doc graphs regenerated.
This commit is contained in:
@@ -59,7 +59,7 @@ forever:
|
||||
boundary = session.deriveMessages() ⟵ reconstruction boundary: same sync frame,
|
||||
session('step/start') strictly before step/start
|
||||
config = waterfall agent/request ⟵ frozen seed; return a replacement to switch
|
||||
reqMsgs = waterfall agent/request-messages ⟵ request-only before/after messages; recorded
|
||||
reqMsgs = waterfall agent/request-advice ⟵ request-only before/after messages; recorded
|
||||
on the header, never session history
|
||||
session('request/header'[-delta]) ⟵ the header event this request owes the log
|
||||
stream llm.stream(freeze({header..., messages: before+boundary+after})) → session('assistant/chunk')
|
||||
@@ -86,7 +86,7 @@ Cancellation: `agent.cancel()` is the single public stop primitive — it clears
|
||||
### What is NOT here
|
||||
|
||||
Everything that goes beyond "call the model, run the tools, repeat" belongs to plugins listening on the event taxonomy:
|
||||
- Hooks: `agent/session-start`, `agent/prompt-submit`, `agent/pre-step`, `agent/request`, `agent/request-messages`, `agent/step-result`, `tools/pre-execute`, `tools/post-execute`, `agent/turn-continuation`
|
||||
- Hooks: `agent/session-start`, `agent/prompt-submit`, `agent/pre-step`, `agent/request`, `agent/request-advice`, `agent/step-result`, `tools/pre-execute`, `tools/post-execute`, `agent/turn-continuation`
|
||||
- Compaction: `agent/pre-step`
|
||||
- Sandbox, permission, plan mode: `tools/pre-execute` (deny/ask gate), `tools/post-execute`
|
||||
- Sub-agents: implemented outside the loop as `ctx.subagents` providers; in-process providers use `ctx.agents.create()` and owned `AgentHandle` teardown, while child streaming/progress and background/poll collection remain deferred.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import type { Context } from 'cordis'
|
||||
import type { FinishReason, GenerateOptions, LlmCallConfig, Message } from '@deepseek-ai/dsh-llm'
|
||||
import { BlockAssembler, HarnessError, deepFreeze } from '@deepseek-ai/dsh-llm'
|
||||
import type { ContinuationDecision, HookContext, PromptDecision, RequestMessages } from '@deepseek-ai/dsh-agent'
|
||||
import type { ContinuationDecision, HookContext, PromptDecision, RequestAdvice } from '@deepseek-ai/dsh-agent'
|
||||
import { canonicalHeader } from '@deepseek-ai/dsh-session'
|
||||
import type { Session, TurnEndReason, TurnTrigger } from '@deepseek-ai/dsh-session'
|
||||
import { createTransmissionLog, recordRequestHeader } from './request-log.ts'
|
||||
@@ -161,7 +161,7 @@ export interface LoopHandle {
|
||||
* boundary = session.deriveMessages() ⟵ the reconstruction boundary: snapshot in the
|
||||
* session('step/start') same sync frame, strictly before step/start
|
||||
* config = waterfall agent/request(config) ⟵ frozen seed; a returned replacement switches
|
||||
* reqMsgs = waterfall agent/request-messages ⟵ request-only before/after messages; logged on
|
||||
* advice = waterfall agent/request-advice ⟵ request-only before/after advice; logged on
|
||||
* the header, never session history
|
||||
* session('request/header'|'request/header-delta') ⟵ the header event this request owes the
|
||||
* log (initial/resume anchor, delta, fallback)
|
||||
@@ -720,32 +720,35 @@ async function runStep(
|
||||
throw new Error(`agent "${agent.id}" has no model: set AgentOptions.model or supply one via the agent/request waterfall`)
|
||||
}
|
||||
|
||||
// Collect request-ONLY messages: `before` contributions precede the boundary
|
||||
// snapshot in the request, `after` contributions follow it. They are not
|
||||
// session history — the header event below is their only durable record
|
||||
// (EpochHeader.messagePrefix/messageSuffix), which keeps the request a pure
|
||||
// function of the log. The frozen empty seed serves both the listener chain
|
||||
// and the no-listener fallback: a contribution is a RETURNED extension of
|
||||
// `await next()`, never an in-place push. Fired AFTER the boundary snapshot,
|
||||
// so a listener's session append lands past the boundary and joins the NEXT
|
||||
// Collect the request-ONLY advice: `before` messages go in front of the
|
||||
// entire boundary snapshot, `after` messages follow its last message. Advice
|
||||
// is not session history — the header event below is its only durable
|
||||
// record (EpochHeader.messagePrefix/messageSuffix), which keeps the request
|
||||
// a pure function of the log. The frozen empty seed serves both the
|
||||
// listener chain and the no-listener fallback: a contribution is a RETURNED
|
||||
// extension of `await next()`, never an in-place push. The context gets a
|
||||
// frozen COPY of the boundary (the request is built from the internal
|
||||
// snapshot), so a listener cannot smuggle unlogged content into the request
|
||||
// by mutating what it was shown. Fired AFTER the boundary snapshot, so a
|
||||
// listener's session append lands past the boundary and joins the NEXT
|
||||
// request — the same window rule as the `agent/request` waterfall.
|
||||
const emptyRequestMessages: RequestMessages = deepFreeze({ before: [], after: [] })
|
||||
const requestMessagesBoundary = deepFreeze([...boundaryMessages])
|
||||
const requestMessages = await ctx.waterfall(
|
||||
'agent/request-messages', agent, turn, step, emptyRequestMessages,
|
||||
{ system, assembly, boundaryMessages: requestMessagesBoundary, signal },
|
||||
() => Promise.resolve(emptyRequestMessages),
|
||||
const emptyRequestAdvice: RequestAdvice = deepFreeze({ before: [], after: [] })
|
||||
const requestAdviceBoundary = deepFreeze([...boundaryMessages])
|
||||
const requestAdvice = await ctx.waterfall(
|
||||
'agent/request-advice', agent, turn, step, emptyRequestAdvice,
|
||||
{ system, assembly, boundaryMessages: requestAdviceBoundary, signal },
|
||||
() => Promise.resolve(emptyRequestAdvice),
|
||||
)
|
||||
|
||||
// The request header (the log's request/header* vocabulary): canonical form,
|
||||
// recorded before dispatch so the log always explains the request —
|
||||
// including the request-only messages, which no other event carries.
|
||||
// including the request-only advice, which no other event carries.
|
||||
const header = canonicalHeader({
|
||||
config,
|
||||
...system ? { system } : {},
|
||||
...assembly.tools.length > 0 ? { tools: assembly.tools } : {},
|
||||
...requestMessages.before.length > 0 ? { messagePrefix: requestMessages.before } : {},
|
||||
...requestMessages.after.length > 0 ? { messageSuffix: requestMessages.after } : {},
|
||||
...requestAdvice.before.length > 0 ? { messagePrefix: requestAdvice.before } : {},
|
||||
...requestAdvice.after.length > 0 ? { messageSuffix: requestAdvice.after } : {},
|
||||
})
|
||||
recordRequestHeader(session, transmission, header)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import AgentRegistry, {
|
||||
AgentId,
|
||||
type ContinuationDecision,
|
||||
type PromptDecision,
|
||||
type RequestMessages,
|
||||
type RequestAdvice,
|
||||
type SessionStartSource,
|
||||
} from '@deepseek-ai/dsh-agent'
|
||||
import AgentLoop, { type ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
|
||||
@@ -311,7 +311,7 @@ describe('agent/session-start', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('agent/request-messages (RequestMessages)', () => {
|
||||
describe('agent/request-advice (RequestAdvice)', () => {
|
||||
it('frames the derived history: before precedes it, after follows it, and the header records both', async () => {
|
||||
const adapter = new MockAdapter([textResponse('ok')])
|
||||
const ctx = await harness(adapter)
|
||||
@@ -319,7 +319,7 @@ describe('agent/request-messages (RequestMessages)', () => {
|
||||
|
||||
const reminder: Message = { role: 'user', content: [{ type: 'text', text: '<system-reminder>catalog</system-reminder>' }] }
|
||||
const trailer: Message = { role: 'user', content: [{ type: 'text', text: 'trailing note' }] }
|
||||
ctx.on('agent/request-messages', async (_agent, _turn, _step, _messages, _context, next): Promise<RequestMessages> => {
|
||||
ctx.on('agent/request-advice', async (_agent, _turn, _step, _messages, _context, next): Promise<RequestAdvice> => {
|
||||
const result = await next()
|
||||
return { before: [...result.before, reminder], after: [...result.after, trailer] }
|
||||
})
|
||||
@@ -351,7 +351,7 @@ describe('agent/request-messages (RequestMessages)', () => {
|
||||
const agent = ctx.agentLoop.create(AgentId('a1'), { model: 'mock' })
|
||||
|
||||
const seen: { system: string; boundaryRoles: string[]; sectionCount: number }[] = []
|
||||
ctx.on('agent/request-messages', async (_agent, _turn, _step, _messages, context, next): Promise<RequestMessages> => {
|
||||
ctx.on('agent/request-advice', async (_agent, _turn, _step, _messages, context, next): Promise<RequestAdvice> => {
|
||||
const result = await next()
|
||||
seen.push({
|
||||
system: context.system,
|
||||
@@ -360,7 +360,7 @@ describe('agent/request-messages (RequestMessages)', () => {
|
||||
})
|
||||
return { before: [{ role: 'user', content: [{ type: 'text', text: 'first' }] }, ...result.before], after: result.after }
|
||||
})
|
||||
ctx.on('agent/request-messages', async (_agent, _turn, _step, _messages, _context, next): Promise<RequestMessages> => {
|
||||
ctx.on('agent/request-advice', async (_agent, _turn, _step, _messages, _context, next): Promise<RequestAdvice> => {
|
||||
const result = await next()
|
||||
return { before: [...result.before, { role: 'user', content: [{ type: 'text', text: 'second' }] }], after: result.after }
|
||||
})
|
||||
@@ -385,7 +385,7 @@ describe('agent/request-messages (RequestMessages)', () => {
|
||||
const agent = ctx.agentLoop.create(AgentId('a1'), { model: 'mock' })
|
||||
|
||||
// A listener that delegates without contributing — the canonical no-op.
|
||||
ctx.on('agent/request-messages', async (_agent, _turn, _step, _messages, _context, next) => next())
|
||||
ctx.on('agent/request-advice', async (_agent, _turn, _step, _messages, _context, next) => next())
|
||||
|
||||
send(agent, 'hi')
|
||||
await waitForIdle(ctx, agent)
|
||||
@@ -402,7 +402,7 @@ describe('agent/request-messages (RequestMessages)', () => {
|
||||
const agent = ctx.agentLoop.create(AgentId('a1'), { model: 'mock' })
|
||||
|
||||
let mutationError: unknown
|
||||
ctx.on('agent/request-messages', async (_agent, _turn, _step, messages, _context, next): Promise<RequestMessages> => {
|
||||
ctx.on('agent/request-advice', async (_agent, _turn, _step, messages, _context, next): Promise<RequestAdvice> => {
|
||||
try {
|
||||
messages.before.push({ role: 'user', content: [{ type: 'text', text: 'smuggled' }] })
|
||||
} catch (error: unknown) {
|
||||
@@ -424,7 +424,7 @@ describe('agent/request-messages (RequestMessages)', () => {
|
||||
const agent = ctx.agentLoop.create(AgentId('a1'), { model: 'mock' })
|
||||
|
||||
let mutationError: unknown
|
||||
ctx.on('agent/request-messages', async (_agent, _turn, _step, _messages, context, next): Promise<RequestMessages> => {
|
||||
ctx.on('agent/request-advice', async (_agent, _turn, _step, _messages, context, next): Promise<RequestAdvice> => {
|
||||
try {
|
||||
const mutableBoundary = context.boundaryMessages as Message[]
|
||||
mutableBoundary.push({ role: 'user', content: [{ type: 'text', text: 'smuggled' }] })
|
||||
@@ -454,7 +454,7 @@ describe('agent/request-messages (RequestMessages)', () => {
|
||||
const agent = ctx.agentLoop.create(AgentId('a1'), { model: 'mock' })
|
||||
|
||||
let step = 0
|
||||
ctx.on('agent/request-messages', async (_agent, _turn, _step, _messages, _context, next): Promise<RequestMessages> => {
|
||||
ctx.on('agent/request-advice', async (_agent, _turn, _step, _messages, _context, next): Promise<RequestAdvice> => {
|
||||
const result = await next()
|
||||
step += 1
|
||||
return { before: [...result.before, { role: 'user', content: [{ type: 'text', text: `reminder v${step}` }] }], after: result.after }
|
||||
|
||||
@@ -45,7 +45,7 @@ Turn and step boundaries are NOT mirrored as `agent/*` emits: a consumer that ne
|
||||
- `agent/prompt-submit` — decide what happens to one drained queued message before it becomes a `user/message`: `PromptDecision` = `allow` (optionally rewriting the prompt `content` or attaching `additionalContext`) or `block` (drop it; a batch whose every prompt is blocked opens a zero-step turn that ends `rejected`). Maps onto Claude Code's `UserPromptSubmit`.
|
||||
- `agent/pre-step` (serial) — mutate the session surface before the step opens and history is derived (compaction). Fires after `turn/start` and before `step/start`, so a listener's appended events land outside the step.
|
||||
- `agent/request` — shape the call config before the model call: a frozen `LlmCallConfig` seed in, a replacement out (model switching, sampling overrides). Content is not shapeable here — every request is a pure function of the session log ([reconstructability RFC](../../../docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.md)); the loop logs whatever config the request actually uses as a `request/header*` event
|
||||
- `agent/request-messages` — contribute request-ONLY messages around the derived history: a frozen empty `RequestMessages` seed in, an extension of `await next()` out (`before` messages precede the boundary snapshot in the request, `after` messages follow it). For per-request advisory context the model must see now but that must not become durable history; the loop records the contributions on the request's `request/header*` event (`EpochHeader.messagePrefix`/`messageSuffix`), so `deriveMessages()` stays untouched and the request stays reconstructable. Cost model: contributions ride the request's uncached tail and are re-paid at full price on every request they appear in — put session-frozen content in `before` (cacheable prefix; a mid-session change busts the cache for everything after it), route low-frequency change notices through `agent.inject()` instead (paid once, prefix-cached thereafter), and reserve `after` for small, frequently refreshed state snapshots
|
||||
- `agent/request-advice` — contribute request-ONLY messages around the derived history: a frozen empty `RequestAdvice` seed in, an extension of `await next()` out (`before` messages precede the boundary snapshot in the request, `after` messages follow it). For per-request advisory context the model must see now but that must not become durable history; the loop records the contributions on the request's `request/header*` event (`EpochHeader.messagePrefix`/`messageSuffix`), so `deriveMessages()` stays untouched and the request stays reconstructable. Cost model: contributions ride the request's uncached tail and are re-paid at full price on every request they appear in — put session-frozen content in `before` (cacheable prefix; a mid-session change busts the cache for everything after it), route low-frequency change notices through `agent.inject()` instead (paid once, prefix-cached thereafter), and reserve `after` for small, frequently refreshed state snapshots
|
||||
- `agent/step-result` — post-process the assembled assistant message before tool dispatch (validates what the log records)
|
||||
- `agent/turn-continuation` — override the continue/stop decision via `ContinuationDecision` = `{action:'stop'}` or `{action:'continue', reason?}` (a `continue` `reason` is recorded as next-step steering in the same turn — the typed `/goal` pattern). Force-continue `/loop`, force-stop budget guard.
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* consumer that wants the live transcript subscribes here.
|
||||
* - **`agent/*`** (this module) — the LIVE runtime surface. Always carries the
|
||||
* live `Agent`. Two shapes: INTERCEPTION seams (the `agent/prompt-submit`/
|
||||
* `agent/request`/`agent/request-messages`/`agent/step-result`/
|
||||
* `agent/request`/`agent/request-advice`/`agent/step-result`/
|
||||
* `agent/turn-continuation` waterfalls and
|
||||
* the serial `agent/pre-step`) that mutate/veto, and TRANSIENT emits
|
||||
* (`agent/status`, `agent/error`, `agent/created`/
|
||||
@@ -156,34 +156,39 @@ export type ContinuationDecision =
|
||||
| { action: 'continue'; reason?: HookContext }
|
||||
|
||||
/**
|
||||
* Request-ONLY messages an `agent/request-messages` waterfall listener
|
||||
* contributes around the derived history of ONE LLM request: `before` messages
|
||||
* precede the derived history in `GenerateOptions.messages`, `after` messages
|
||||
* follow it. They are NOT session events — nothing here enters the session log
|
||||
* as durable history, `Session.deriveMessages()` never returns them, and the
|
||||
* next step recomputes them from scratch. The loop records the non-empty
|
||||
* arrays on the request's `request/header*` event (`EpochHeader.messagePrefix`
|
||||
* / `messageSuffix`), so the request stays reconstructable from the log (the
|
||||
* reconstructability RFC). For content that must become durable conversation
|
||||
* history, use the log channels instead: `agent.inject()`, steering, or
|
||||
* prompt-submit `additionalContext`.
|
||||
* The request-only ADVICE an `agent/request-advice` waterfall listener weaves
|
||||
* around the derived history of ONE LLM request — advice in both senses:
|
||||
* advisory content for the model, attached before/after the join point like
|
||||
* AOP advice, never modifying the history itself. In
|
||||
* `GenerateOptions.messages` the `before` messages sit in front of the ENTIRE
|
||||
* derived history (directly after the provider's system slot) and the `after`
|
||||
* messages follow its last message (the newest user prompt on a turn's first
|
||||
* step, the previous step's tool results afterwards). Advice is NOT session
|
||||
* state — nothing here enters the session log as durable history,
|
||||
* `Session.deriveMessages()` never returns it, and the next step recomputes
|
||||
* it from scratch. The loop records the non-empty arrays on the request's
|
||||
* `request/header*` event (`EpochHeader.messagePrefix` / `messageSuffix`), so
|
||||
* the request stays reconstructable from the log (the reconstructability
|
||||
* RFC). For content that must become durable conversation history, use the
|
||||
* log channels instead: `agent.inject()`, steering, or prompt-submit
|
||||
* `additionalContext`.
|
||||
*/
|
||||
export interface RequestMessages {
|
||||
/** Messages placed before the derived history in the request. */
|
||||
export interface RequestAdvice {
|
||||
/** Before-advice: messages placed ahead of the entire derived history. */
|
||||
before: Message[]
|
||||
/** Messages placed after the derived history in the request. */
|
||||
/** After-advice: messages placed after the derived history's last message. */
|
||||
after: Message[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Read-only facts about the request an `agent/request-messages` listener is
|
||||
* Read-only facts about the request an `agent/request-advice` listener is
|
||||
* contributing to. Everything here is already fixed when the seam fires: the
|
||||
* step is open, the boundary snapshot is taken, and the system prompt is
|
||||
* assembled — a listener uses these to DECIDE what to contribute (e.g. render
|
||||
* a workspace-dependent reminder, or skip one already present in history),
|
||||
* never to mutate them.
|
||||
*/
|
||||
export interface RequestMessagesContext {
|
||||
export interface RequestAdviceContext {
|
||||
/** The rendered system prompt this request will carry. */
|
||||
system: string
|
||||
/** The prompt assembly the system prompt was rendered from (sections + tools). */
|
||||
@@ -412,7 +417,7 @@ declare module 'cordis' {
|
||||
* session log (the reconstructability RFC), so model-visible content
|
||||
* flows through the log channels — `inject()`, steering, prompt-submit
|
||||
* `additionalContext`, prompt sections via `system-prompt/assemble`, or
|
||||
* header-logged request-only messages via {@link agent/request-messages}
|
||||
* header-logged request-only messages via {@link agent/request-advice}
|
||||
* — never through request mutation, and the loop records whatever config
|
||||
* the request actually uses as a `request/header*` event before dispatch.
|
||||
* The step's messages are already snapshotted when this fires (the
|
||||
@@ -429,10 +434,11 @@ declare module 'cordis' {
|
||||
*/
|
||||
'agent/request'(agent: Agent, turn: number, step: number, config: LlmCallConfig, next: () => Promise<LlmCallConfig>): Promise<LlmCallConfig>
|
||||
/**
|
||||
* Waterfall: contribute request-ONLY messages around the derived history —
|
||||
* a {@link RequestMessages} whose `before` messages precede the boundary
|
||||
* snapshot in `GenerateOptions.messages` and whose `after` messages follow
|
||||
* it. Fires once per step, inside the open step, after the
|
||||
* Waterfall: weave request-ONLY advice around the derived history — a
|
||||
* {@link RequestAdvice} whose `before` messages sit in front of the
|
||||
* ENTIRE boundary snapshot in `GenerateOptions.messages` and whose
|
||||
* `after` messages follow its last message. Fires once per step, inside
|
||||
* the open step, after the
|
||||
* {@link agent/request} config waterfall and before the loop logs the
|
||||
* request header. This is the seam for per-request advisory context the
|
||||
* model must see NOW but that must NOT become durable history (a skills
|
||||
@@ -443,13 +449,13 @@ declare module 'cordis' {
|
||||
* reconstructable from the log.
|
||||
*
|
||||
* The seed is frozen and empty; a contributing listener returns a NEW
|
||||
* {@link RequestMessages} extending `await next()` (spread its arrays —
|
||||
* {@link RequestAdvice} extending `await next()` (spread its arrays —
|
||||
* never mutate them), so contributions compose across plugins in
|
||||
* registration order. The boundary snapshot is already taken when this
|
||||
* fires: a `session.append`/`inject()` from a listener here lands in the
|
||||
* log but joins the NEXT request — contribute through the returned value,
|
||||
* not the session. Call `next()` to delegate, or return a
|
||||
* {@link RequestMessages} without it to short-circuit.
|
||||
* {@link RequestAdvice} without it to short-circuit.
|
||||
*
|
||||
* Pick the channel by change frequency (the cost model): a contribution
|
||||
* rides the request's uncached tail, re-tokenized at full price on EVERY
|
||||
@@ -464,11 +470,11 @@ declare module 'cordis' {
|
||||
* @param agent - the agent making the model call.
|
||||
* @param turn - the open turn number.
|
||||
* @param step - the step whose request this is.
|
||||
* @param messages - the frozen empty seed; return an extended replacement to contribute.
|
||||
* @param context - read-only request facts ({@link RequestMessagesContext}).
|
||||
* @param advice - the frozen empty seed; return an extended replacement to contribute.
|
||||
* @param context - read-only request facts ({@link RequestAdviceContext}).
|
||||
* @mode waterfall
|
||||
*/
|
||||
'agent/request-messages'(agent: Agent, turn: number, step: number, messages: RequestMessages, context: RequestMessagesContext, next: () => Promise<RequestMessages>): Promise<RequestMessages>
|
||||
'agent/request-advice'(agent: Agent, turn: number, step: number, advice: RequestAdvice, context: RequestAdviceContext, next: () => Promise<RequestAdvice>): Promise<RequestAdvice>
|
||||
/**
|
||||
* Waterfall: post-process the assembled assistant {@link Message} before
|
||||
* tool dispatch (validation, content rewriting, …).
|
||||
|
||||
@@ -51,7 +51,7 @@ Plain class (not a Cordis Service). Create via `ctx.sessions.create()`.
|
||||
|
||||
### Request-header reconstruction (`request-header.ts`)
|
||||
|
||||
The `request/header` (full `EpochHeader` snapshot with a `RequestHeaderReason`) and `request/header-delta` (system line-trim / name-keyed tools delta / whole config / whole request-only message arrays) events make the request envelope logged session state, so every conversation request is a pure function of the log. The pure trio reconstructs it: `foldRequestHeader(events)` folds a log (or any prefix) into the header in force; `diffHeader(prev, next)` encodes a change (undefined when equal); `applyHeaderDelta(prev, delta)` replays one. Writer contract: every logged delta is round-trip-verified (`apply(prev, delta)` deep-equals the new header) with a `'fallback'` snapshot when the encoding cannot express the change (a pure tool reordering), so folding never needs error recovery on a well-formed log. `canonicalHeader` pins the one representation of absence (empty system/tools/messagePrefix/messageSuffix ≡ absent fields; a delta's EMPTY message array encodes the transition back to absence). `EpochHeader.messagePrefix`/`messageSuffix` are the durable record of the `agent/request-messages` waterfall's request-only contributions — the request is `messagePrefix + derived history + messageSuffix`, and `deriveMessages()` never returns them.
|
||||
The `request/header` (full `EpochHeader` snapshot with a `RequestHeaderReason`) and `request/header-delta` (system line-trim / name-keyed tools delta / whole config / whole request-only message arrays) events make the request envelope logged session state, so every conversation request is a pure function of the log. The pure trio reconstructs it: `foldRequestHeader(events)` folds a log (or any prefix) into the header in force; `diffHeader(prev, next)` encodes a change (undefined when equal); `applyHeaderDelta(prev, delta)` replays one. Writer contract: every logged delta is round-trip-verified (`apply(prev, delta)` deep-equals the new header) with a `'fallback'` snapshot when the encoding cannot express the change (a pure tool reordering), so folding never needs error recovery on a well-formed log. `canonicalHeader` pins the one representation of absence (empty system/tools/messagePrefix/messageSuffix ≡ absent fields; a delta's EMPTY message array encodes the transition back to absence). `EpochHeader.messagePrefix`/`messageSuffix` are the durable record of the `agent/request-advice` waterfall's request-only contributions — the request is `messagePrefix + derived history + messageSuffix`, and `deriveMessages()` never returns them.
|
||||
|
||||
### Session event vocabulary (`types.ts`)
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ export interface EpochHeader {
|
||||
tools?: ToolSchema[]
|
||||
/**
|
||||
* Request-only messages sent BEFORE the derived history (the
|
||||
* `agent/request-messages` waterfall's `before` contributions). Not session
|
||||
* `agent/request-advice` waterfall's `before` contributions). Not session
|
||||
* history — `deriveMessages()` never returns them — so the header is their
|
||||
* only durable record; absent when the request carried none.
|
||||
*/
|
||||
|
||||
@@ -368,7 +368,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
||||
// be EXACTLY what the session log reconstructs:
|
||||
//
|
||||
// - messages: the folded header's request-only messages (messagePrefix /
|
||||
// messageSuffix — the `agent/request-messages` contributions, logged on
|
||||
// messageSuffix — the `agent/request-advice` contributions, logged on
|
||||
// the header because no session event carries them) framing the
|
||||
// derivation over the log prefix strictly before the in-flight step's
|
||||
// `step/start` (the reconstruction boundary). The derivation is compared
|
||||
|
||||
Reference in New Issue
Block a user