@@ -104,6 +104,26 @@ type Logger = {
104104 error : ( msg : string ) => void ;
105105} ;
106106
107+ interface ContextBudgets {
108+ archiveMemory : number ;
109+ sessionContext : number ;
110+ reserved : number ;
111+ }
112+
113+ const BUDGET_UNLIMITED = - 1 ;
114+ const ARCHIVE_BUDGET_RATIO = 0.15 ;
115+ const ARCHIVE_BUDGET_CAP = 8_000 ;
116+ const RESERVED_MIN = 20_000 ;
117+ const RESERVED_RATIO = 0.15 ;
118+ const ARCHIVE_INDEX_TRIM_LIMIT = 10 ;
119+
120+ function allocateContextBudget ( totalBudget : number ) : ContextBudgets {
121+ const reserved = Math . max ( totalBudget * RESERVED_RATIO , RESERVED_MIN ) ;
122+ const archiveMemory = Math . min ( totalBudget * ARCHIVE_BUDGET_RATIO , ARCHIVE_BUDGET_CAP ) ;
123+ const sessionContext = Math . max ( totalBudget - archiveMemory - reserved , 0 ) ;
124+ return { archiveMemory, sessionContext, reserved } ;
125+ }
126+
107127function estimateTokens ( messages : AgentMessage [ ] ) : number {
108128 return Math . max ( 1 , messages . length * 80 ) ;
109129}
@@ -419,6 +439,70 @@ function buildSystemPromptAddition(): string {
419439 ] . join ( "\n" ) ;
420440}
421441
442+ function buildInstructionPrompt ( ) : { text : string ; tokens : number } {
443+ const text = buildSystemPromptAddition ( ) ;
444+ return { text, tokens : Math . ceil ( text . length / 4 ) } ;
445+ }
446+
447+ function buildArchiveMemory (
448+ archiveOverview : string | undefined ,
449+ preAbstracts : Array < { archive_id : string ; abstract : string } > ,
450+ budget : number ,
451+ ) : { messages : AgentMessage [ ] ; tokens : number } {
452+ const messages : AgentMessage [ ] = [ ] ;
453+
454+ if ( archiveOverview ) {
455+ messages . push ( {
456+ role : "user" ,
457+ content : `[Session History Summary]\n${ archiveOverview } ` ,
458+ } ) ;
459+ }
460+
461+ if ( preAbstracts . length > 0 ) {
462+ const lines = preAbstracts . map ( ( a ) => `${ a . archive_id } : ${ a . abstract } ` ) ;
463+ messages . push ( {
464+ role : "user" ,
465+ content : `[Archive Index]\n${ lines . join ( "\n" ) } ` ,
466+ } ) ;
467+ }
468+
469+ let tokens = roughEstimate ( messages ) ;
470+ if ( budget === BUDGET_UNLIMITED || tokens <= budget || preAbstracts . length <= ARCHIVE_INDEX_TRIM_LIMIT ) {
471+ return { messages, tokens } ;
472+ }
473+
474+ const trimmed = preAbstracts . slice ( - ARCHIVE_INDEX_TRIM_LIMIT ) ;
475+ const trimmedMessages : AgentMessage [ ] = [ ] ;
476+ if ( archiveOverview ) {
477+ trimmedMessages . push ( {
478+ role : "user" ,
479+ content : `[Session History Summary]\n${ archiveOverview } ` ,
480+ } ) ;
481+ }
482+ trimmedMessages . push ( {
483+ role : "user" ,
484+ content : `[Archive Index]\n${ trimmed . map ( ( a ) => `${ a . archive_id } : ${ a . abstract } ` ) . join ( "\n" ) } ` ,
485+ } ) ;
486+ tokens = roughEstimate ( trimmedMessages ) ;
487+ return { messages : trimmedMessages , tokens } ;
488+ }
489+
490+ function buildSessionContext (
491+ ovMessages : OVMessage [ ] ,
492+ budget : number ,
493+ ) : { messages : AgentMessage [ ] ; tokens : number } {
494+ const messages = ovMessages . flatMap ( ( m ) => convertToAgentMessages ( m ) ) ;
495+ const tokens = roughEstimate ( messages ) ;
496+ if ( budget === BUDGET_UNLIMITED || tokens <= budget ) {
497+ return { messages, tokens } ;
498+ }
499+ const trimmed = [ ...messages ] ;
500+ while ( trimmed . length > 0 && roughEstimate ( trimmed ) > budget ) {
501+ trimmed . shift ( ) ;
502+ }
503+ return { messages : trimmed , tokens : roughEstimate ( trimmed ) } ;
504+ }
505+
422506function sleep ( ms : number ) : Promise < void > {
423507 return new Promise ( ( resolve ) => setTimeout ( resolve , ms ) ) ;
424508}
@@ -598,6 +682,64 @@ export function createMemoryOpenVikingContextEngine(params: {
598682 return false ;
599683 }
600684
685+ function assemblePassthrough (
686+ ovSessionId : string ,
687+ reason : string ,
688+ liveMessages : AgentMessage [ ] ,
689+ originalTokens : number ,
690+ extra ?: Record < string , unknown > ,
691+ ) : AssembleResult {
692+ diag ( "assemble_result" , ovSessionId , {
693+ passthrough : true ,
694+ reason,
695+ outputMessagesCount : liveMessages . length ,
696+ inputTokenEstimate : originalTokens ,
697+ estimatedTokens : originalTokens ,
698+ tokensSaved : 0 ,
699+ savingPct : 0 ,
700+ ...extra ,
701+ } ) ;
702+ return { messages : liveMessages , estimatedTokens : originalTokens } ;
703+ }
704+
705+ function buildAssembledContext (
706+ overview : string | undefined ,
707+ preAbstracts : Array < { archive_id : string ; abstract : string } > ,
708+ ovMessages : OVMessage [ ] ,
709+ tokenBudget : number ,
710+ ovSessionId : string ,
711+ ) : {
712+ sanitized : AgentMessage [ ] ;
713+ archive : { messages : AgentMessage [ ] ; tokens : number } ;
714+ session : { messages : AgentMessage [ ] ; tokens : number } ;
715+ budgets : ContextBudgets ;
716+ instruction : { text : string ; tokens : number } ;
717+ } {
718+ // 4-layer context partitioning (budget computed for diag; BUDGET_UNLIMITED bypasses limits):
719+ // Instruction — system prompt guide (Archive Index / Session History usage)
720+ // Archive — session history summary + per-archive one-line abstracts
721+ // Session — active OV messages converted to AgentMessage format
722+ // Reserved — headroom for model output (not consumed here)
723+ const budgets = allocateContextBudget ( tokenBudget ) ;
724+ const instruction = buildInstructionPrompt ( ) ;
725+ const archive = buildArchiveMemory ( overview , preAbstracts , BUDGET_UNLIMITED ) ;
726+ const session = buildSessionContext ( ovMessages , BUDGET_UNLIMITED ) ;
727+ const assembled = [ ...archive . messages , ...session . messages ] ;
728+
729+ logger . info (
730+ `openviking: assemble entering session content for ${ ovSessionId } : ` +
731+ JSON . stringify ( assembled . map ( ( m ) => ( {
732+ role : m . role ,
733+ content : typeof m . content === "string" ? m . content . substring ( 0 , 100 ) : "[complex]" ,
734+ } ) ) , null , 2 ) ,
735+ ) ;
736+
737+ normalizeAssistantContent ( assembled ) ;
738+ const sanitized = sanitizeToolUseResultPairing ( assembled as never [ ] ) as AgentMessage [ ] ;
739+
740+ return { sanitized, archive, session, budgets, instruction } ;
741+ }
742+
601743 return {
602744 info : {
603745 id,
@@ -641,131 +783,72 @@ export function createMemoryOpenVikingContextEngine(params: {
641783 } ) ;
642784
643785 if ( isBypassedSession ( { sessionId : assembleParams . sessionId , sessionKey } ) ) {
644- diag ( "assemble_result" , OVSessionId , {
645- passthrough : true ,
646- reason : "session_bypassed" ,
647- outputMessagesCount : messages . length ,
648- inputTokenEstimate : originalTokens ,
649- estimatedTokens : originalTokens ,
650- tokensSaved : 0 ,
651- savingPct : 0 ,
652- } ) ;
653- return { messages, estimatedTokens : originalTokens } ;
786+ return assemblePassthrough ( OVSessionId , "session_bypassed" , messages , originalTokens ) ;
654787 }
655788
656789 try {
657- if ( ! ( await runLocalPrecheck ( "assemble" , OVSessionId , {
658- tokenBudget,
659- } ) ) ) {
790+ if ( ! ( await runLocalPrecheck ( "assemble" , OVSessionId , { tokenBudget } ) ) ) {
660791 return { messages, estimatedTokens : roughEstimate ( messages ) } ;
661792 }
662793 const client = await getClient ( ) ;
663- const routingRef =
664- assembleParams . sessionId ?? sessionKey ?? OVSessionId ;
794+ const routingRef = assembleParams . sessionId ?? sessionKey ?? OVSessionId ;
665795 const agentId = resolveAgentId ( routingRef , sessionKey , OVSessionId ) ;
666- const ctx = await client . getSessionContext (
667- OVSessionId ,
668- tokenBudget ,
669- agentId ,
670- ) ;
796+ const ctx = await client . getSessionContext ( OVSessionId , tokenBudget , agentId ) ;
671797
672798 const preAbstracts = ctx ?. pre_archive_abstracts ?? [ ] ;
673799 const hasArchives = ! ! ctx ?. latest_archive_overview || preAbstracts . length > 0 ;
674800 const activeCount = ctx ?. messages ?. length ?? 0 ;
675801
676802 if ( ! ctx || ( ! hasArchives && activeCount === 0 ) ) {
677- diag ( "assemble_result" , OVSessionId , {
678- passthrough : true , reason : "no_ov_data" ,
803+ return assemblePassthrough ( OVSessionId , "no_ov_data" , messages , originalTokens , {
679804 archiveCount : 0 , activeCount : 0 ,
680- outputMessagesCount : messages . length ,
681- inputTokenEstimate : originalTokens ,
682- estimatedTokens : originalTokens ,
683- tokensSaved : 0 , savingPct : 0 ,
684805 } ) ;
685- return { messages, estimatedTokens : roughEstimate ( messages ) } ;
686806 }
687-
688807 if ( ! hasArchives && ctx . messages . length < messages . length ) {
689- diag ( "assemble_result" , OVSessionId , {
690- passthrough : true , reason : "ov_msgs_fewer_than_input" ,
808+ return assemblePassthrough ( OVSessionId , "ov_msgs_fewer_than_input" , messages , originalTokens , {
691809 archiveCount : 0 , activeCount,
692- outputMessagesCount : messages . length ,
693- inputTokenEstimate : originalTokens ,
694- estimatedTokens : originalTokens ,
695- tokensSaved : 0 , savingPct : 0 ,
696- } ) ;
697- return { messages, estimatedTokens : roughEstimate ( messages ) } ;
698- }
699-
700- const assembled : AgentMessage [ ] = [ ] ;
701-
702- if ( ctx . latest_archive_overview ) {
703- assembled . push ( {
704- role : "user" as const ,
705- content : `[Session History Summary]\n${ ctx . latest_archive_overview } ` ,
706- } ) ;
707- }
708-
709- if ( preAbstracts . length > 0 ) {
710- const lines : string [ ] = preAbstracts . map (
711- ( a ) => `${ a . archive_id } : ${ a . abstract } ` ,
712- ) ;
713- assembled . push ( {
714- role : "user" as const ,
715- content : `[Archive Index]\n${ lines . join ( "\n" ) } ` ,
716810 } ) ;
717811 }
718812
719- assembled . push ( ...ctx . messages . flatMap ( ( m ) => convertToAgentMessages ( m ) ) ) ;
720-
721- // 打印进入 session 的完整内容
722- logger . info (
723- `openviking: assemble entering session content for ${ OVSessionId } : ` +
724- JSON . stringify ( assembled . map ( ( m ) => ( {
725- role : m . role ,
726- content : typeof m . content === "string" ? m . content . substring ( 0 , 100 ) : "[complex]" ,
727- } ) ) , null , 2 ) ,
813+ const { sanitized, archive, session, budgets, instruction } = buildAssembledContext (
814+ ctx . latest_archive_overview ,
815+ preAbstracts ,
816+ ctx . messages ,
817+ tokenBudget ,
818+ OVSessionId ,
728819 ) ;
729820
730- normalizeAssistantContent ( assembled ) ;
731- const sanitized = sanitizeToolUseResultPairing ( assembled as never [ ] ) as AgentMessage [ ] ;
732-
733821 if ( sanitized . length === 0 && messages . length > 0 ) {
734- diag ( "assemble_result" , OVSessionId , {
735- passthrough : true , reason : "sanitized_empty" ,
736- archiveCount : preAbstracts . length ,
737- activeCount,
738- outputMessagesCount : messages . length ,
739- inputTokenEstimate : originalTokens ,
740- estimatedTokens : originalTokens ,
741- tokensSaved : 0 , savingPct : 0 ,
822+ return assemblePassthrough ( OVSessionId , "sanitized_empty" , messages , originalTokens , {
823+ archiveCount : preAbstracts . length , activeCount,
742824 } ) ;
743- return { messages, estimatedTokens : roughEstimate ( messages ) } ;
744825 }
745826
746827 const assembledTokens = roughEstimate ( sanitized ) ;
747- const archiveCount = preAbstracts . length ;
748828 const tokensSaved = originalTokens - assembledTokens ;
749829 const savingPct = originalTokens > 0 ? Math . round ( ( tokensSaved / originalTokens ) * 100 ) : 0 ;
750830
751831 diag ( "assemble_result" , OVSessionId , {
752832 passthrough : false ,
753- archiveCount,
833+ archiveCount : preAbstracts . length ,
754834 activeCount,
755835 outputMessagesCount : sanitized . length ,
756836 inputTokenEstimate : originalTokens ,
757837 estimatedTokens : assembledTokens ,
758838 tokensSaved,
759839 savingPct,
840+ archiveTokens : archive . tokens ,
841+ archiveBudget : budgets . archiveMemory ,
842+ sessionTokens : session . tokens ,
843+ sessionBudget : budgets . sessionContext ,
844+ reservedBudget : budgets . reserved ,
760845 messages : messageDigest ( sanitized ) ,
761846 } ) ;
762847
763848 return {
764849 messages : sanitized ,
765- estimatedTokens : ctx . estimatedTokens ,
766- ...( hasArchives
767- ? { systemPromptAddition : buildSystemPromptAddition ( ) }
768- : { } ) ,
850+ estimatedTokens : assembledTokens ,
851+ ...( hasArchives ? { systemPromptAddition : instruction . text } : { } ) ,
769852 } ;
770853 } catch ( err ) {
771854 logger . warn ?.(
0 commit comments