loop: every request is built from the log — boundary snapshot, header events, config-only waterfall
The loop is now transmission-stateless; a request is a pure function of (session log, this step's rendered assembly, current AgentOptions): - The reconstruction boundary is step/start: the messages snapshot is taken in the same synchronous frame immediately before the step/start append, so the request's messages are exactly the derivation over events[0..stepStartSeq) — an inject() from an agent/request listener (or any concurrent task) lands after the boundary and joins the NEXT request. This changes behavior for a synchronous step/start session/event listener that appends content (master derived after the append, so such a listener could reach the current request): agent/pre-step is the sanctioned seam for current-request content. - agent/request is re-typed to config-only: (agent, turn, step, config: LlmCallConfig, next) → LlmCallConfig. The frozen seed comes from AgentOptions on a loop instance's first request (explicit options beat the logged baseline — fork overrides and resume reconfiguration stay correct) and from the log's folded header afterwards; listeners return a replacement to switch. Content shaping through the request is no longer expressible — model-visible content flows through the log channels. - recordRequestHeader appends whatever header event the request owes the log before dispatch: an 'initial'/'resume' snapshot anchoring each loop instance, a round-trip-verified delta on change, a 'fallback' snapshot when the encoding cannot express it. Session.requestHeader() is the log's incrementally-folded baseline. - Requests are deep-frozen before dispatch (deepFreeze exempts the AbortSignal — freezing one breaks AbortController.abort() outright); frozen + sessionId is the loop-built marker the dev invariant keys on. Ported from #162 and re-anchored on the log: the append-extension / frozen-end-to-end / compaction-resend / prompt-change property tests, plus new specs for the boundary semantics, resume anchoring, and the end-to-end theorem (every recorded request rebuilds byte-equal from the log alone). Live cache-hit e2e (request-cache.e2e.ts) verified against the real DeepSeek API. Snapshot goldens intentionally stale until the single re-record after the compact/summary envelope lands.
This commit is contained in:
@@ -45,7 +45,10 @@ export function callConfigEquals(a: LlmCallConfig, b: LlmCallConfig): boolean {
|
||||
* never rewrite it, so the wire bytes cannot silently desync from what the
|
||||
* session log reconstructs. Guards against cycles with a WeakSet: loop-built
|
||||
* requests hold `structuredClone`d JSON-validated session data, but the
|
||||
* helper accepts arbitrarily constructed values.
|
||||
* helper accepts arbitrarily constructed values. One exemption: an
|
||||
* `AbortSignal` is never entered or frozen — it is the request's live
|
||||
* cancellation channel, and freezing one breaks `AbortController.abort()`
|
||||
* outright (Node stores the aborted flag as an own property of the signal).
|
||||
* @param value - the value to freeze in place.
|
||||
* @returns the same value, frozen.
|
||||
*/
|
||||
@@ -53,6 +56,7 @@ export function deepFreeze<T>(value: T): T {
|
||||
const seen = new WeakSet<object>()
|
||||
const walk = (node: unknown): void => {
|
||||
if (node === null || typeof node !== 'object') return
|
||||
if (node instanceof AbortSignal) return
|
||||
if (seen.has(node)) return
|
||||
seen.add(node)
|
||||
Object.freeze(node)
|
||||
|
||||
@@ -33,6 +33,18 @@ describe('deepFreeze', () => {
|
||||
expect(() => { (value.a.b[1] as { c: string }).c = 'y' }).toThrow(TypeError)
|
||||
})
|
||||
|
||||
it('never freezes an AbortSignal: the live cancellation channel keeps working', () => {
|
||||
const controller = new AbortController()
|
||||
const request = deepFreeze({ model: 'm', signal: controller.signal })
|
||||
expect(Object.isFrozen(request)).toBe(true)
|
||||
expect(Object.isFrozen(controller.signal)).toBe(false)
|
||||
let fired = false
|
||||
controller.signal.addEventListener('abort', () => { fired = true }, { once: true })
|
||||
controller.abort('stop')
|
||||
expect(fired).toBe(true)
|
||||
expect(controller.signal.aborted).toBe(true)
|
||||
})
|
||||
|
||||
it('passes primitives through and terminates on cycles', () => {
|
||||
expect(deepFreeze(42)).toBe(42)
|
||||
expect(deepFreeze(null)).toBeNull()
|
||||
|
||||
Reference in New Issue
Block a user