D4: token budget — rolling transcript summary keeps long sessions bounded
Director sessions can run for hours; the context would grow without bound. Now a fixed live window of recent turns is sent each turn, and older turns are folded into a rolling per-session summary. - planContext (pure): summary covers [0, summarizedThrough); the live window is the rest. Once the window exceeds windowSize + SUMMARIZE_BATCH, the oldest turns fold down to the last windowSize — never leaving a gap between summary and window. - maybeSummarize (after each turn): folds the batch into AiSession.summary via a separate complete() call when a key is set, else a deterministic compaction (one compact line per turn, capped) — bounded with or without an LLM. - The summary already rides in the system prompt (it never displaces the grounded roster), so the closed-set anti-hallucination anchor is preserved as history scrolls. Pure windowing + deterministic-summary covered by unit tests. 395 tests green; lint clean; build OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,13 +6,43 @@ import { getLlmConfig, useAssistantStore } from '@/stores/assistantStore';
|
||||
import { useDirectorStore } from '@/stores/directorStore';
|
||||
import { useCharacters } from '@/features/characters/hooks';
|
||||
import { useEncounters } from '@/features/combat/hooks';
|
||||
import { buildDirectorScene, runDirectorTurn, type DirectorAction, type RollRequest } from '@/lib/assistant/director';
|
||||
import {
|
||||
buildDirectorScene,
|
||||
runDirectorTurn,
|
||||
planContext,
|
||||
deterministicSummary,
|
||||
buildSummaryPrompt,
|
||||
type DirectorAction,
|
||||
type RollRequest,
|
||||
} from '@/lib/assistant/director';
|
||||
import { complete } from '@/lib/llm/client';
|
||||
import type { Degree } from '@/lib/dice/check';
|
||||
import { useLatestAiSession } from './hooks';
|
||||
import { useDirectorAction, type ApplyResult } from './useDirectorAction';
|
||||
|
||||
const nowIso = (): string => new Date().toISOString();
|
||||
|
||||
/**
|
||||
* Fold older transcript turns into the session's rolling summary when the live
|
||||
* window has grown past its budget. Uses the LLM when configured, else a
|
||||
* deterministic compaction — so the context stays bounded with or without a key.
|
||||
*/
|
||||
async function maybeSummarize(sessionId: string, systemConstraint: string, windowSize: number): Promise<void> {
|
||||
const s = await aiSessionsRepo.get(sessionId);
|
||||
if (!s) return;
|
||||
const plan = planContext(s.transcript, s.summarizedThrough, windowSize);
|
||||
if (plan.foldEntries.length === 0) return;
|
||||
|
||||
const cfg = getLlmConfig();
|
||||
let summary = deterministicSummary(s.summary, plan.foldEntries);
|
||||
if (cfg.enabled && cfg.apiKey.trim()) {
|
||||
const { system, user } = buildSummaryPrompt(systemConstraint, s.summary, plan.foldEntries);
|
||||
const res = await complete(cfg, { system, user, maxTokens: 512 });
|
||||
if (res.ok && 'text' in res && res.text.trim()) summary = res.text.trim();
|
||||
}
|
||||
await aiSessionsRepo.setSummary(sessionId, summary, plan.newSummarizedThrough);
|
||||
}
|
||||
|
||||
const DEGREE_LABEL: Record<Degree, string> = {
|
||||
'critical-success': 'Critical Success',
|
||||
success: 'Success',
|
||||
@@ -68,7 +98,7 @@ export function useDirectorSession(campaign: Campaign) {
|
||||
}
|
||||
|
||||
const fresh = await aiSessionsRepo.get(sid);
|
||||
const transcriptWindow = (fresh?.transcript ?? []).slice(-windowSize);
|
||||
const plan = planContext(fresh?.transcript ?? [], fresh?.summarizedThrough ?? 0, windowSize);
|
||||
const scene = buildDirectorScene({
|
||||
campaign,
|
||||
characters,
|
||||
@@ -76,7 +106,7 @@ export function useDirectorSession(campaign: Campaign) {
|
||||
persona,
|
||||
controlledCharacterId,
|
||||
narrationStyle,
|
||||
transcriptWindow,
|
||||
transcriptWindow: plan.window,
|
||||
summary: fresh?.summary,
|
||||
});
|
||||
|
||||
@@ -94,6 +124,9 @@ export function useDirectorSession(campaign: Campaign) {
|
||||
}]);
|
||||
}
|
||||
setLastTurn({ rollRequests: res.turn.rollRequests, actions: res.turn.actions, suggestions: res.turn.suggestions });
|
||||
|
||||
// Token budget: fold older turns into a rolling summary so the context stays bounded.
|
||||
await maybeSummarize(sid, scene.systemConstraint, windowSize);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user