@@ -344,8 +344,136 @@ async def mock_generate(request, stream=False, ctx=None):
344344 visible_events = [event for event in result_events if event .is_model_visible ()]
345345 assert len (visible_events ) == 4 # 1 summary + 3 recent
346346 assert any (event .is_summary_event () for event in result_events )
347+ summary_event = next (event for event in result_events if event .is_summary_event ())
348+ assert summary_event .author == "system"
349+ assert summary_event .content .role == "user"
347350
348- async def test_summary_without_keep_recent (self ):
351+ async def test_summary_traces_back_to_invisible_user_before_first_visible_event (self ):
352+ model = _make_model_mock ()
353+ llm_response = MagicMock ()
354+ llm_response .content = Content (parts = [Part .from_text (text = "summary text" )])
355+ captured_prompts = []
356+
357+ async def mock_generate (request , stream = False , ctx = None ):
358+ captured_prompts .append (request .contents [0 ].parts [0 ].text )
359+ yield llm_response
360+
361+ model .generate_async = mock_generate
362+ summarizer = SessionSummarizer (model = model , start_by_user_turn = True )
363+ hidden_user = _make_event (author = "user" , text = "hidden question" )
364+ hidden_user .set_model_visible (False )
365+ old_answer = _make_event (author = "agent" , text = "visible answer" )
366+ recent_user = _make_event (author = "user" , text = "recent question" )
367+ system_preamble = _make_event (author = "system" , text = "system preamble" )
368+ system_preamble .set_model_visible (False )
369+ events = [
370+ system_preamble ,
371+ hidden_user ,
372+ old_answer ,
373+ recent_user ,
374+ ]
375+
376+ summary_text , result_events = await summarizer .create_session_summary_by_events (
377+ events , "s1" , keep_recent_count = 1 )
378+
379+ assert summary_text == "summary text"
380+ assert result_events is events
381+ assert captured_prompts
382+ assert "hidden question" in captured_prompts [0 ]
383+ assert "visible answer" in captured_prompts [0 ]
384+ assert "system preamble" not in captured_prompts [0 ]
385+ assert "recent question" not in captured_prompts [0 ]
386+ assert old_answer .is_model_visible () is False
387+ assert recent_user .is_model_visible () is True
388+ assert any (event .is_summary_event () for event in result_events )
389+
390+ async def test_summary_can_start_from_existing_summary_event (self ):
391+ model = _make_model_mock ()
392+ llm_response = MagicMock ()
393+ llm_response .content = Content (parts = [Part .from_text (text = "summary text" )])
394+ captured_prompts = []
395+
396+ async def mock_generate (request , stream = False , ctx = None ):
397+ captured_prompts .append (request .contents [0 ].parts [0 ].text )
398+ yield llm_response
399+
400+ model .generate_async = mock_generate
401+ summarizer = SessionSummarizer (model = model , start_by_user_turn = True )
402+ existing_summary = _make_event (author = "system" , text = "previous summary" )
403+ existing_summary .set_summary_event (True )
404+ system_preamble = _make_event (author = "system" , text = "system preamble" )
405+ system_preamble .set_model_visible (False )
406+ events = [
407+ system_preamble ,
408+ existing_summary ,
409+ _make_event (author = "agent" , text = "old answer" ),
410+ _make_event (author = "user" , text = "recent question" ),
411+ ]
412+
413+ summary_text , result_events = await summarizer .create_session_summary_by_events (
414+ events , "s1" , keep_recent_count = 1 )
415+
416+ assert summary_text == "summary text"
417+ assert "previous summary" in captured_prompts [0 ]
418+ assert "old answer" in captured_prompts [0 ]
419+ assert "system preamble" not in captured_prompts [0 ]
420+ assert result_events [3 ].is_summary_event ()
421+
422+ async def test_summary_falls_back_to_first_visible_event_and_ignores_large_keep_recent (self ):
423+ model = _make_model_mock ()
424+ llm_response = MagicMock ()
425+ llm_response .content = Content (parts = [Part .from_text (text = "summary text" )])
426+ captured_prompts = []
427+
428+ async def mock_generate (request , stream = False , ctx = None ):
429+ captured_prompts .append (request .contents [0 ].parts [0 ].text )
430+ yield llm_response
431+
432+ model .generate_async = mock_generate
433+ summarizer = SessionSummarizer (model = model , start_by_user_turn = True )
434+ events = [
435+ _make_event (author = "agent" , text = "agent message 1" ),
436+ _make_event (author = "agent" , text = "agent message 2" ),
437+ ]
438+
439+ summary_text , result_events = await summarizer .create_session_summary_by_events (
440+ events , "s1" , keep_recent_count = 10 )
441+
442+ assert summary_text == "summary text"
443+ assert "agent message 1" in captured_prompts [0 ]
444+ assert "agent message 2" in captured_prompts [0 ]
445+ visible_events = [event for event in result_events if event .is_model_visible ()]
446+ assert len (visible_events ) == 1
447+ assert visible_events [0 ].is_summary_event ()
448+
449+ async def test_summary_inserted_before_recent_user_turn_and_hides_prior_events (self ):
450+ model = _make_model_mock ()
451+ llm_response = MagicMock ()
452+ llm_response .content = Content (parts = [Part .from_text (text = "summary text" )])
453+ captured_prompts = []
454+
455+ async def mock_generate (request , stream = False , ctx = None ):
456+ captured_prompts .append (request .contents [0 ].parts [0 ].text )
457+ yield llm_response
458+
459+ model .generate_async = mock_generate
460+ summarizer = SessionSummarizer (model = model , start_by_user_turn = True )
461+ events = [_make_event (author = "user" if idx in (8 , 80 , 92 ) else "agent" , text = f"msg { idx } " ) for idx in range (100 )]
462+ for idx , event in enumerate (events ):
463+ event .set_model_visible (10 <= idx < 99 )
464+
465+ summary_text , result_events = await summarizer .create_session_summary_by_events (
466+ events , "s1" , keep_recent_count = 10 )
467+
468+ assert summary_text == "summary text"
469+ assert "msg 8" in captured_prompts [0 ]
470+ assert "msg 91" in captured_prompts [0 ]
471+ assert "msg 92" not in captured_prompts [0 ]
472+ assert result_events [92 ].is_summary_event ()
473+ assert all (not event .is_model_visible () for event in result_events [:92 ])
474+ assert result_events [93 ].is_model_visible ()
475+
476+ async def test_summary_with_zero_keep_recent (self ):
349477 model = _make_model_mock ()
350478 llm_response = MagicMock ()
351479 llm_response .content = Content (parts = [Part .from_text (text = "summary text" )])
@@ -357,11 +485,13 @@ async def mock_generate(request, stream=False, ctx=None):
357485 summarizer = SessionSummarizer (model = model )
358486 events = [_make_event (text = f"msg{ i } " ) for i in range (5 )]
359487 summary_text , result_events = await summarizer .create_session_summary_by_events (
360- events , "s1" , keep_recent_count = None )
488+ events , "s1" , keep_recent_count = 0 )
361489 assert summary_text is not None
362490 assert len (result_events ) == 6 # preserve all original events + 1 summary
363491 visible_events = [event for event in result_events if event .is_model_visible ()]
364492 assert len (visible_events ) == 1 # only summary event remains model-visible
493+ assert visible_events [0 ].is_summary_event ()
494+ assert visible_events [0 ].content .role == "user"
365495
366496 async def test_summary_no_events (self ):
367497 model = _make_model_mock ()
@@ -407,6 +537,67 @@ async def mock_generate(request, stream=False, ctx=None):
407537 assert len (visible_events ) == 3 # 1 summary + 2 recent
408538 assert any (event .is_summary_event () for event in session .events )
409539
540+ async def test_summary_traces_back_to_invisible_user_before_visible_events (self ):
541+ model = _make_model_mock ()
542+ llm_response = MagicMock ()
543+ llm_response .content = Content (parts = [Part .from_text (text = "session summary" )])
544+ captured_prompts = []
545+
546+ async def mock_generate (request , stream = False , ctx = None ):
547+ captured_prompts .append (request .contents [0 ].parts [0 ].text )
548+ yield llm_response
549+
550+ model .generate_async = mock_generate
551+ summarizer = SessionSummarizer (model = model , keep_recent_count = 1 , start_by_user_turn = True )
552+ hidden_user = _make_event (author = "user" , text = "hidden question" )
553+ hidden_user .set_model_visible (False )
554+ old_answer = _make_event (author = "agent" , text = "visible answer" )
555+ recent_user = _make_event (author = "user" , text = "recent question" )
556+ system_preamble = _make_event (author = "system" , text = "system preamble" )
557+ system_preamble .set_model_visible (False )
558+ session = _make_session (events = [
559+ system_preamble ,
560+ hidden_user ,
561+ old_answer ,
562+ recent_user ,
563+ ])
564+
565+ result = await summarizer .create_session_summary (session )
566+
567+ assert result == "session summary"
568+ assert captured_prompts
569+ assert "hidden question" in captured_prompts [0 ]
570+ assert "visible answer" in captured_prompts [0 ]
571+ assert "system preamble" not in captured_prompts [0 ]
572+ assert "recent question" not in captured_prompts [0 ]
573+ assert old_answer .is_model_visible () is False
574+ assert recent_user .is_model_visible () is True
575+ assert any (event .is_summary_event () for event in session .events )
576+
577+ async def test_summary_without_visible_user_falls_back_to_first_visible_event (self ):
578+ model = _make_model_mock ()
579+ llm_response = MagicMock ()
580+ llm_response .content = Content (parts = [Part .from_text (text = "session summary" )])
581+
582+ async def mock_generate (request , stream = False , ctx = None ):
583+ yield llm_response
584+
585+ model .generate_async = mock_generate
586+ summarizer = SessionSummarizer (model = model , keep_recent_count = 10 , start_by_user_turn = True )
587+ events = [
588+ _make_event (author = "system" , text = "system preamble" ),
589+ _make_event (author = "agent" , text = "agent answer" ),
590+ ]
591+ session = _make_session (events = events )
592+
593+ result = await summarizer .create_session_summary (session )
594+
595+ assert result == "session summary"
596+ assert len (session .events ) == 3
597+ visible_events = [event for event in session .events if event .is_model_visible ()]
598+ assert len (visible_events ) == 1
599+ assert visible_events [0 ].is_summary_event ()
600+
410601 async def test_summary_no_update_on_failure (self ):
411602 model = _make_model_mock ()
412603
0 commit comments