@@ -21,13 +21,110 @@ import {
2121import { LOG_NAMESPACE_ROOT } from "../branding.js" ;
2222import { NOOP_TELEMETRY , type Telemetry } from "../telemetry/index.js" ;
2323import {
24- buildArchiveSummaryExcerpt ,
25- type SummaryExcerptArchive ,
26- } from "./summary-excerpt.js" ;
24+ formatArchiveRef ,
25+ type CompactionArchive ,
26+ } from "./compaction-archive.js" ;
27+ import type {
28+ ArchiveKind ,
29+ ArchiveOccurrence ,
30+ } from "./compaction-archive-schema.js" ;
2731import { readSourceCredentialMaterial } from "../config/source-credentials.js" ;
2832
2933const logger = getLogger ( [ LOG_NAMESPACE_ROOT , "session" , "summarizer" ] ) ;
3034
35+ // Token-budgeted compaction excerpt from the evidence archive.
36+ //
37+ // The live transcript is a clipped view. The archive holds the authorized
38+ // payloads compaction is about to drop, so the summary call should read those
39+ // rather than 400-character stubs. Budget is the control: later kinds yield
40+ // when earlier ones fill the window. Gap rows contribute metadata only.
41+ export const SUMMARY_EXCERPT_DEFAULT_BUDGET_CHARS = 80_000 ;
42+
43+ const KIND_PRIORITY : readonly ArchiveKind [ ] = [
44+ "user_message" ,
45+ "attachment" ,
46+ "assistant_text" ,
47+ "tool_args" ,
48+ "tool_failure" ,
49+ "tool_result" ,
50+ "overflow_blob" ,
51+ ] ;
52+
53+ export type SummaryExcerptArchive = Pick <
54+ CompactionArchive ,
55+ "listOccurrences" | "readAuthorizedPayload"
56+ > ;
57+
58+ function heading ( occ : ArchiveOccurrence ) : string {
59+ const parts = [ `### ${ occ . kind } ${ formatArchiveRef ( occ . occurrenceId ) } ` ] ;
60+ if ( occ . callId !== undefined ) parts . push ( `call=${ occ . callId } ` ) ;
61+ if ( occ . lifecycle !== undefined ) parts . push ( `lifecycle=${ occ . lifecycle } ` ) ;
62+ if ( occ . gap === true ) parts . push ( "[gap]" ) ;
63+ return parts . join ( " " ) ;
64+ }
65+
66+ /**
67+ * Build a budgeted, kind-prioritized excerpt for the compaction summary call.
68+ * Empty archives return "" so the caller can fall back to the live transcript.
69+ */
70+ export async function buildArchiveSummaryExcerpt (
71+ archive : SummaryExcerptArchive ,
72+ budgetChars = SUMMARY_EXCERPT_DEFAULT_BUDGET_CHARS ,
73+ ) : Promise < string > {
74+ const occurrences = await archive . listOccurrences ( ) ;
75+ if ( occurrences . length === 0 ) return "" ;
76+
77+ const byKind = new Map < ArchiveKind , ArchiveOccurrence [ ] > ( ) ;
78+ for ( const occ of occurrences ) {
79+ const list = byKind . get ( occ . kind ) ;
80+ if ( list !== undefined ) list . push ( occ ) ;
81+ else byKind . set ( occ . kind , [ occ ] ) ;
82+ }
83+
84+ const sections : string [ ] = [ ] ;
85+ let used = 0 ;
86+ let omitted = 0 ;
87+
88+ for ( const kind of KIND_PRIORITY ) {
89+ const group = byKind . get ( kind ) ;
90+ if ( group === undefined ) continue ;
91+ for ( const occ of group ) {
92+ const remaining = budgetChars - used ;
93+ if ( remaining <= 0 ) {
94+ omitted ++ ;
95+ continue ;
96+ }
97+
98+ let body : string | undefined ;
99+ if ( occ . gap === true ) {
100+ body = "(payload not stored)" ;
101+ } else {
102+ try {
103+ body = await archive . readAuthorizedPayload ( occ . occurrenceId ) ;
104+ } catch {
105+ omitted ++ ;
106+ continue ;
107+ }
108+ }
109+
110+ const section = `${ heading ( occ ) } \n${ body } ` ;
111+ const separator = sections . length > 0 ? 2 : 0 ;
112+ if ( section . length + separator > remaining ) {
113+ omitted ++ ;
114+ continue ;
115+ }
116+ sections . push ( section ) ;
117+ used += section . length + separator ;
118+ }
119+ }
120+
121+ const excerpt = sections . join ( "\n\n" ) ;
122+ if ( omitted === 0 ) return excerpt ;
123+ const note = `${ omitted } occurrence${ omitted === 1 ? "" : "s" } omitted` ;
124+ if ( excerpt . length === 0 ) return note ;
125+ return `${ excerpt } \n\n${ note } ` ;
126+ }
127+
31128// What the agent was doing when compaction fired. Lets the summary preserve
32129// the workflow contract ("we are at step 3/7 of /build") rather than dropping
33130// it into the compacted region.
0 commit comments