feat(compact): add optional cancellation signal to the compact seam methods

This commit is contained in:
Hypatia May
2026-06-23 15:45:35 +08:00
parent bdaf9f651d
commit f4180bd764
4 changed files with 47 additions and 6 deletions

View File

@@ -18,8 +18,10 @@ Both methods are **abstract** — the backend owns the entire strategy (token es
| Member | Semantics |
|---|---|
| `compactIfNeeded(session, systemPrompt?, model?)` | Estimate the history size; if over the backend's threshold, compact an older range via `compactRegion`, keeping recent context intact. Returns the `CompactionResult`, or `null` if nothing needed compacting. |
| `compactRegion(session, start, end, model)` | Forcibly summarize surface nodes `[start, end]` (inclusive seqs) into a single replacement node. **Throws** if a compaction is already in progress, if `start`/`end` aren't surface nodes, or if `start > end`. |
| `compactIfNeeded(session, systemPrompt?, model?, signal?)` | Estimate the history size; if over the backend's threshold, compact an older range via `compactRegion`, keeping recent context intact. Returns the `CompactionResult`, or `null` if nothing needed compacting. |
| `compactRegion(session, start, end, model, signal?)` | Forcibly summarize surface nodes `[start, end]` (inclusive seqs) into a single replacement node. **Throws** if a compaction is already in progress, if `start`/`end` aren't surface nodes, or if `start > end`. |
Both methods take an optional `signal: AbortSignal`. A backend that summarizes via `ctx.llm.stream()` **must** forward it into the call's `GenerateOptions.signal`, so an abort or fiber dispose tears down the in-flight summarization instead of leaving an orphaned model call running past the cancellation. The turn that the `compact/*` events belong to is not a parameter — it is recoverable from the log (the currently-open turn), so the backend stamps it without the caller supplying it.
## Surface contract

View File

@@ -69,12 +69,17 @@ export abstract class CompactService extends Service {
* @param session - the session whose surface may be compacted.
* @param systemPrompt - optional system prompt, counted toward the estimate.
* @param model - optional summarization model (falls back to backend config).
* @param signal - optional cancellation signal. A backend that summarizes 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
* leaving an orphaned model call running past the cancellation.
* @returns the compaction result, or `null` if no compaction was needed.
*/
abstract compactIfNeeded(
session: Session,
systemPrompt?: string,
model?: string,
signal?: AbortSignal,
): Promise<CompactionResult | null>
/**
@@ -88,6 +93,10 @@ export abstract class CompactService extends Service {
* @param start - inclusive seq of the first surface node to compact.
* @param end - inclusive seq of the last surface node to compact.
* @param model - summarization model.
* @param signal - optional cancellation signal. A backend that summarizes 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
* leaving an orphaned model call running past the cancellation.
* @throws if compaction is already in progress, or if `start`/`end` are not
* valid surface nodes, or if `start > end`.
*/
@@ -96,6 +105,7 @@ export abstract class CompactService extends Service {
start: number,
end: number,
model: string,
signal?: AbortSignal,
): Promise<CompactionResult>
}

View File

@@ -11,11 +11,27 @@ import { Session, SessionId } from '@deepseek-ai/dsh-session'
* declaration merge.
*/
class StubCompactService extends CompactService {
override async compactIfNeeded(_session: Session, _systemPrompt?: string, _model?: string): Promise<CompactionResult | null> {
/** Records the signal handed to the most recent call, to prove it threads through. */
lastSignal: AbortSignal | undefined
override async compactIfNeeded(
_session: Session,
_systemPrompt?: string,
_model?: string,
signal?: AbortSignal,
): Promise<CompactionResult | null> {
this.lastSignal = signal
return null
}
override async compactRegion(session: Session, start: number, end: number, _model: string): Promise<CompactionResult> {
override async compactRegion(
session: Session,
start: number,
end: number,
_model: string,
signal?: AbortSignal,
): Promise<CompactionResult> {
this.lastSignal = signal
// Minimal stub honoring the lock + log-only event contract.
const startEvent = session.append('compact/start', { turn: 0 })
const summaryEvent = session.append('compact/summary', {
@@ -75,4 +91,17 @@ describe('CompactService seam', () => {
expect(result.summarySeq).toBeGreaterThan(result.startSeq)
expect(result.endSeq).toBeGreaterThan(result.summarySeq)
})
it('threads the cancellation signal through to the backend', async () => {
const ctx = new Context()
const svc = new StubCompactService(ctx)
const session = new Session(SessionId('s'))
const controller = new AbortController()
await svc.compactRegion(session, 0, 0, 'm', controller.signal)
expect(svc.lastSignal).toBe(controller.signal)
await svc.compactIfNeeded(session, undefined, undefined, controller.signal)
expect(svc.lastSignal).toBe(controller.signal)
})
})