@@ -395,6 +395,109 @@ def _create_summary(self, messages: List[Dict[str, Any]]) -> str:
395395 return ""
396396
397397
398+ class SummarizeToolOutputsOptimizer (BaseOptimizer ):
399+ """
400+ Summarize large tool outputs using LLM before truncation.
401+
402+ This optimizer specifically targets tool role messages with large content,
403+ using an LLM to create intelligent summaries that preserve key information.
404+ Falls back to keeping original content if LLM is unavailable or fails.
405+ """
406+
407+ def __init__ (
408+ self ,
409+ llm_summarize_fn : Optional [callable ] = None ,
410+ max_output_tokens : int = 1000 ,
411+ min_chars_to_summarize : int = 2000 ,
412+ preserve_recent : int = 2 ,
413+ tool_summarize_limits : Optional [Dict [str , int ]] = None ,
414+ ):
415+ """
416+ Initialize tool output summarizer.
417+
418+ Args:
419+ llm_summarize_fn: Function(content, max_tokens) -> summary string
420+ max_output_tokens: Target token count for summarized output
421+ min_chars_to_summarize: Default minimum chars before summarization triggers
422+ preserve_recent: Number of recent tool outputs to preserve intact
423+ tool_summarize_limits: Per-tool min_chars_to_summarize limits {tool_name: min_chars}
424+ """
425+ self .llm_summarize_fn = llm_summarize_fn
426+ self .max_output_tokens = max_output_tokens
427+ self .min_chars_to_summarize = min_chars_to_summarize
428+ self .preserve_recent = preserve_recent
429+ self .tool_summarize_limits = tool_summarize_limits or {}
430+
431+ def optimize (
432+ self ,
433+ messages : List [Dict [str , Any ]],
434+ target_tokens : int ,
435+ ledger : Optional [ContextLedger ] = None ,
436+ ) -> tuple :
437+ original_tokens = estimate_messages_tokens (messages )
438+
439+ # If already under budget or no LLM function, return as-is
440+ if original_tokens <= target_tokens or not self .llm_summarize_fn :
441+ return messages , OptimizationResult (
442+ original_tokens = original_tokens ,
443+ optimized_tokens = original_tokens ,
444+ tokens_saved = 0 ,
445+ strategy_used = OptimizerStrategy .SMART ,
446+ )
447+
448+ result = []
449+ summarized_count = 0
450+
451+ # Find tool messages and their indices
452+ tool_indices = [i for i , m in enumerate (messages ) if m .get ("role" ) == "tool" ]
453+
454+ # Preserve recent tool outputs (only if there are more than preserve_recent tools)
455+ if tool_indices and self .preserve_recent > 0 and len (tool_indices ) > self .preserve_recent :
456+ recent_tool_indices = set (tool_indices [- self .preserve_recent :])
457+ else :
458+ recent_tool_indices = set () # Summarize all if few tools or preserve_recent=0
459+
460+ for i , msg in enumerate (messages ):
461+ role = msg .get ("role" , "" )
462+ content = msg .get ("content" , "" )
463+
464+ # Only process tool messages with large content
465+ if role == "tool" and i not in recent_tool_indices :
466+ # Get per-tool limit or use default
467+ tool_name = msg .get ("name" , "" )
468+ min_chars = self .tool_summarize_limits .get (tool_name , self .min_chars_to_summarize )
469+ if isinstance (content , str ) and len (content ) >= min_chars :
470+ # Try to summarize
471+ try :
472+ summary = self .llm_summarize_fn (content , self .max_output_tokens )
473+ if summary and len (summary ) < len (content ):
474+ summarized_msg = msg .copy ()
475+ summarized_msg ["content" ] = summary
476+ summarized_msg ["_summarized" ] = True
477+ summarized_msg ["_original_length" ] = len (content )
478+ result .append (summarized_msg )
479+ summarized_count += 1
480+ continue
481+ except Exception :
482+ # Fallback to original on error
483+ pass
484+
485+ result .append (msg )
486+
487+ optimized_tokens = estimate_messages_tokens (result )
488+
489+ tokens_saved = original_tokens - optimized_tokens
490+
491+ return result , OptimizationResult (
492+ original_tokens = original_tokens ,
493+ optimized_tokens = optimized_tokens ,
494+ tokens_saved = tokens_saved ,
495+ strategy_used = OptimizerStrategy .SMART ,
496+ tool_outputs_summarized = summarized_count ,
497+ tokens_saved_by_summarization = tokens_saved , # All savings from summarization
498+ )
499+
500+
398501class LLMSummarizeOptimizer (SummarizeOptimizer ):
399502 """
400503 LLM-powered summarization optimizer.
@@ -475,9 +578,10 @@ class SmartOptimizer(BaseOptimizer):
475578 Smart optimization combining multiple strategies.
476579
477580 Applies strategies in order:
478- 1. Prune tool outputs
479- 2. Sliding window
480- 3. Summarize if still over
581+ 1. Summarize tool outputs (if LLM available and smart_tool_summarize=True)
582+ 2. Prune tool outputs (fallback truncation)
583+ 3. Sliding window
584+ 4. Summarize conversation if still over
481585 """
482586
483587 def __init__ (
@@ -486,11 +590,21 @@ def __init__(
486590 protected_tools : Optional [List [str ]] = None ,
487591 tool_limits : Optional [Dict [str , int ]] = None ,
488592 llm_summarize_fn : Optional [callable ] = None ,
593+ smart_tool_summarize : bool = True ,
594+ tool_summarize_limits : Optional [Dict [str , int ]] = None ,
489595 ):
490596 self .preserve_recent = preserve_recent
491597 self .protected_tools = protected_tools or []
492598 self .tool_limits = tool_limits or {}
599+ self .smart_tool_summarize = smart_tool_summarize
600+ self .tool_summarize_limits = tool_summarize_limits or {}
493601
602+ # Tool output summarization (LLM-powered, applied first when available)
603+ self ._summarize_tools = SummarizeToolOutputsOptimizer (
604+ llm_summarize_fn = llm_summarize_fn if smart_tool_summarize else None ,
605+ preserve_recent = preserve_recent ,
606+ tool_summarize_limits = tool_summarize_limits ,
607+ )
494608 self ._prune = PruneToolsOptimizer (
495609 preserve_recent = preserve_recent ,
496610 protected_tools = protected_tools ,
@@ -518,19 +632,43 @@ def optimize(
518632 strategy_used = OptimizerStrategy .SMART ,
519633 )
520634
521- # Step 1: Prune tool outputs
522- result , prune_result = self ._prune .optimize (messages , target_tokens , ledger )
635+ # Step 1: Summarize tool outputs (LLM-powered, if available)
636+ tool_summarized_count = 0
637+ tokens_saved_by_summarization = 0
638+ if self ._summarize_tools .llm_summarize_fn :
639+ result , tool_summary_result = self ._summarize_tools .optimize (messages , target_tokens , ledger )
640+ tool_summarized_count = tool_summary_result .tool_outputs_summarized
641+ tokens_saved_by_summarization = tool_summary_result .tokens_saved_by_summarization
642+
643+ if estimate_messages_tokens (result ) <= target_tokens :
644+ return result , OptimizationResult (
645+ original_tokens = original_tokens ,
646+ optimized_tokens = tool_summary_result .optimized_tokens ,
647+ tokens_saved = original_tokens - tool_summary_result .optimized_tokens ,
648+ strategy_used = OptimizerStrategy .SMART ,
649+ tool_outputs_summarized = tool_summarized_count ,
650+ tokens_saved_by_summarization = tokens_saved_by_summarization ,
651+ )
652+ else :
653+ result = messages
654+
655+ # Step 2: Prune tool outputs (fallback truncation)
656+ result , prune_result = self ._prune .optimize (result , target_tokens , ledger )
657+ tokens_saved_by_truncation = prune_result .tokens_saved
523658
524659 if estimate_messages_tokens (result ) <= target_tokens :
525660 return result , OptimizationResult (
526661 original_tokens = original_tokens ,
527662 optimized_tokens = prune_result .optimized_tokens ,
528663 tokens_saved = original_tokens - prune_result .optimized_tokens ,
529664 strategy_used = OptimizerStrategy .SMART ,
665+ tool_outputs_summarized = tool_summarized_count ,
530666 tool_outputs_pruned = prune_result .tool_outputs_pruned ,
667+ tokens_saved_by_summarization = tokens_saved_by_summarization ,
668+ tokens_saved_by_truncation = tokens_saved_by_truncation ,
531669 )
532670
533- # Step 2 : Sliding window
671+ # Step 3 : Sliding window
534672 result , window_result = self ._window .optimize (result , target_tokens , ledger )
535673
536674 if estimate_messages_tokens (result ) <= target_tokens :
@@ -543,7 +681,7 @@ def optimize(
543681 messages_removed = window_result .messages_removed ,
544682 )
545683
546- # Step 3 : Summarize
684+ # Step 4 : Summarize conversation
547685 result , summary_result = self ._summarize .optimize (result , target_tokens , ledger )
548686
549687 optimized_tokens = estimate_messages_tokens (result )
@@ -553,7 +691,10 @@ def optimize(
553691 optimized_tokens = optimized_tokens ,
554692 tokens_saved = original_tokens - optimized_tokens ,
555693 strategy_used = OptimizerStrategy .SMART ,
694+ tool_outputs_summarized = tool_summarized_count ,
556695 tool_outputs_pruned = prune_result .tool_outputs_pruned ,
696+ tokens_saved_by_summarization = tokens_saved_by_summarization ,
697+ tokens_saved_by_truncation = tokens_saved_by_truncation ,
557698 messages_removed = window_result .messages_removed + summary_result .messages_removed ,
558699 summary_added = summary_result .summary_added ,
559700 )
0 commit comments