fix(agent-loop): compose the session prefix before pre-step; hand it to the pressure gate
ds-review-bot critical (follow-up): on the first step of a resumed or seeded/forked instance, auto-compaction ran before runStep composed this instance's prefix, so the gate read the PREVIOUS instance's logged prefix from the header fold — a contributor that grew across resume/fork (skills added, AGENTS.md grown: exactly the environment-dependent case) could under-gate and ship an over-window first request. The loop now composes agent/session-prefix before the instance's first agent/pre-step (still once per instance; runStep just reads the cache), and agent/pre-step carries the composed prefix to its listeners. CompactService.compactIfNeeded gains the sessionPrefix parameter; BasicCompactService.estimatePressure gates on the handed value — the header-fold read is gone, so the estimate is exact at every step including a resumed/forked instance's first. Composition moving before the boundary snapshot also means a composing listener's session append now joins the CURRENT request (documented on the seam). New coverage: composition precedes pre-step and the seam receives the composed prefix; cancel and disposal landing inside the composition window drop the step cleanly; the compact gate test hands the prefix directly.
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
*/
|
||||
|
||||
import { Context, Service } from 'cordis'
|
||||
import type { Message } from '@deepseek-ai/dsh-llm'
|
||||
import type { Session } from '@deepseek-ai/dsh-session'
|
||||
import type { CompactionResult } from './types.ts'
|
||||
|
||||
@@ -68,16 +69,20 @@ export abstract class CompactService extends Service {
|
||||
/**
|
||||
* Check token pressure and compact if the conversation is too large.
|
||||
*
|
||||
* Estimates the current surface-derived history size (including the system
|
||||
* prompt), and if it exceeds the backend's threshold, compacts an older range
|
||||
* Estimates the NEXT request's size — the session prefix, the
|
||||
* surface-derived history, and the system prompt — and if it exceeds the
|
||||
* backend's threshold, compacts an older range
|
||||
* via {@link compactRegion}, keeping recent context intact. Returns `null`
|
||||
* when no compaction is needed.
|
||||
*
|
||||
* Scope and guarantees a backend MUST honor:
|
||||
* - **Surface-derived history only.** The decision is made against the history
|
||||
* derived from the session surface — the only thing compaction can act on.
|
||||
* Non-surface context injected downstream (into the request `messages` by a
|
||||
* later listener) is out of this accounting by construction.
|
||||
* - **Compaction acts on surface-derived history only**, but the ESTIMATE
|
||||
* counts everything the request carries: the loop composes the session
|
||||
* prefix before the pre-step seam fires and hands it here, so the gate
|
||||
* sees the prefix this instance will actually send (`EpochHeader.messagePrefix`
|
||||
* — request-only, never derived history). Non-surface context injected
|
||||
* downstream (into the request `messages` by a later listener) is out of
|
||||
* this accounting by construction.
|
||||
* - **Head-anchored, best-effort.** Auto-compaction consolidates from the
|
||||
* surface HEAD up to a balanced tool-pairing cutoff, so a prior head
|
||||
* checkpoint is
|
||||
@@ -88,10 +93,14 @@ export abstract class CompactService extends Service {
|
||||
* - **Single-unit overflow is out of scope.** If a single retained unit (one
|
||||
* closed step, or a large free node such as a pasted `user/message`) ALONE
|
||||
* exceeds the budget, compaction cannot help and the call may go out
|
||||
* over-budget. Bounding an individual unit's size is a separate concern.
|
||||
* over-budget. Bounding an individual unit's size is a separate concern —
|
||||
* as is a session prefix that alone approaches the window (a
|
||||
* configuration error no compactor fixes: compaction cannot shrink the
|
||||
* prefix).
|
||||
*
|
||||
* @param agent - agent context owning the session surface and model options.
|
||||
* @param fullSystemPrompt - assembled system prompt, counted toward the estimate.
|
||||
* @param sessionPrefix - the instance's composed session prefix, counted toward the estimate.
|
||||
* @param signal - cancellation signal. A backend summarizing via
|
||||
* `ctx.llm.stream()` MUST forward this into the call's `GenerateOptions.signal`
|
||||
* so an abort/dispose tears down the in-flight summarization rather than
|
||||
@@ -101,6 +110,7 @@ export abstract class CompactService extends Service {
|
||||
abstract compactIfNeeded(
|
||||
agent: CompactAgentContext,
|
||||
fullSystemPrompt: string,
|
||||
sessionPrefix: readonly Message[],
|
||||
signal: AbortSignal,
|
||||
): Promise<CompactionResult | null>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { CompactService } from '@deepseek-ai/dsh-compact'
|
||||
import type { CompactionResult } from '@deepseek-ai/dsh-compact'
|
||||
import type { Message } from '@deepseek-ai/dsh-llm'
|
||||
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { CompactAgentContext } from '@deepseek-ai/dsh-compact'
|
||||
|
||||
@@ -18,6 +19,7 @@ class StubCompactService extends CompactService {
|
||||
override async compactIfNeeded(
|
||||
_agent: CompactAgentContext,
|
||||
_fullSystemPrompt: string,
|
||||
_sessionPrefix: readonly Message[],
|
||||
signal: AbortSignal,
|
||||
): Promise<CompactionResult | null> {
|
||||
this.lastSignal = signal
|
||||
@@ -78,7 +80,7 @@ describe('CompactService seam', () => {
|
||||
const ctx = new Context()
|
||||
const svc = new StubCompactService(ctx)
|
||||
const session = new Session(SessionId('s'))
|
||||
expect(await svc.compactIfNeeded(stubAgent(session), '', new AbortController().signal)).toBeNull()
|
||||
expect(await svc.compactIfNeeded(stubAgent(session), '', [], new AbortController().signal)).toBeNull()
|
||||
})
|
||||
|
||||
it('compact/* events merge into SessionEventMap and are log-only', async () => {
|
||||
@@ -107,7 +109,7 @@ describe('CompactService seam', () => {
|
||||
await svc.compactRegion(session, 0, 0, stubAgent(session, 'm'), controller.signal)
|
||||
expect(svc.lastSignal).toBe(controller.signal)
|
||||
|
||||
await svc.compactIfNeeded(stubAgent(session), '', controller.signal)
|
||||
await svc.compactIfNeeded(stubAgent(session), '', [], controller.signal)
|
||||
expect(svc.lastSignal).toBe(controller.signal)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user