@@ -344,6 +344,107 @@ 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"
350+
351+ async def test_summary_filters_visible_events_and_starts_from_user (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+ first_user = _make_event (author = "user" , text = "visible question" )
366+ old_answer = _make_event (author = "agent" , text = "visible answer" )
367+ recent_user = _make_event (author = "user" , text = "recent question" )
368+ events = [
369+ _make_event (author = "system" , text = "system preamble" ),
370+ hidden_user ,
371+ first_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 "visible question" in captured_prompts [0 ]
383+ assert "visible answer" in captured_prompts [0 ]
384+ assert "system preamble" not in captured_prompts [0 ]
385+ assert "hidden question" not in captured_prompts [0 ]
386+ assert "recent question" not in captured_prompts [0 ]
387+ assert first_user .is_model_visible () is False
388+ assert old_answer .is_model_visible () is False
389+ assert recent_user .is_model_visible () is True
390+ assert any (event .is_summary_event () for event in result_events )
391+
392+ async def test_summary_can_start_from_existing_summary_event (self ):
393+ model = _make_model_mock ()
394+ llm_response = MagicMock ()
395+ llm_response .content = Content (parts = [Part .from_text (text = "summary text" )])
396+ captured_prompts = []
397+
398+ async def mock_generate (request , stream = False , ctx = None ):
399+ captured_prompts .append (request .contents [0 ].parts [0 ].text )
400+ yield llm_response
401+
402+ model .generate_async = mock_generate
403+ summarizer = SessionSummarizer (model = model , start_by_user_turn = True )
404+ existing_summary = _make_event (author = "system" , text = "previous summary" )
405+ existing_summary .set_summary_event (True )
406+ events = [
407+ _make_event (author = "system" , text = "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 [1 ].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 ()
347448
348449 async def test_summary_without_keep_recent (self ):
349450 model = _make_model_mock ()
@@ -362,6 +463,8 @@ async def mock_generate(request, stream=False, ctx=None):
362463 assert len (result_events ) == 6 # preserve all original events + 1 summary
363464 visible_events = [event for event in result_events if event .is_model_visible ()]
364465 assert len (visible_events ) == 1 # only summary event remains model-visible
466+ assert visible_events [0 ].is_summary_event ()
467+ assert visible_events [0 ].content .role == "user"
365468
366469 async def test_summary_no_events (self ):
367470 model = _make_model_mock ()
@@ -407,6 +510,69 @@ async def mock_generate(request, stream=False, ctx=None):
407510 assert len (visible_events ) == 3 # 1 summary + 2 recent
408511 assert any (event .is_summary_event () for event in session .events )
409512
513+ async def test_summary_uses_visible_events_starting_from_user (self ):
514+ model = _make_model_mock ()
515+ llm_response = MagicMock ()
516+ llm_response .content = Content (parts = [Part .from_text (text = "session summary" )])
517+ captured_prompts = []
518+
519+ async def mock_generate (request , stream = False , ctx = None ):
520+ captured_prompts .append (request .contents [0 ].parts [0 ].text )
521+ yield llm_response
522+
523+ model .generate_async = mock_generate
524+ summarizer = SessionSummarizer (model = model , keep_recent_count = 1 , start_by_user_turn = True )
525+ hidden_user = _make_event (author = "user" , text = "hidden question" )
526+ hidden_user .set_model_visible (False )
527+ first_user = _make_event (author = "user" , text = "visible question" )
528+ old_answer = _make_event (author = "agent" , text = "visible answer" )
529+ recent_user = _make_event (author = "user" , text = "recent question" )
530+ session = _make_session (events = [
531+ _make_event (author = "system" , text = "system preamble" ),
532+ hidden_user ,
533+ first_user ,
534+ old_answer ,
535+ recent_user ,
536+ ])
537+
538+ result = await summarizer .create_session_summary (session )
539+
540+ assert result == "session summary"
541+ assert captured_prompts
542+ assert "visible question" in captured_prompts [0 ]
543+ assert "visible answer" in captured_prompts [0 ]
544+ assert "system preamble" not in captured_prompts [0 ]
545+ assert "hidden question" not in captured_prompts [0 ]
546+ assert "recent question" not in captured_prompts [0 ]
547+ assert first_user .is_model_visible () is False
548+ assert old_answer .is_model_visible () is False
549+ assert recent_user .is_model_visible () is True
550+ assert any (event .is_summary_event () for event in session .events )
551+
552+ async def test_summary_without_visible_user_falls_back_to_first_visible_event (self ):
553+ model = _make_model_mock ()
554+ llm_response = MagicMock ()
555+ llm_response .content = Content (parts = [Part .from_text (text = "session summary" )])
556+
557+ async def mock_generate (request , stream = False , ctx = None ):
558+ yield llm_response
559+
560+ model .generate_async = mock_generate
561+ summarizer = SessionSummarizer (model = model , keep_recent_count = 10 , start_by_user_turn = True )
562+ events = [
563+ _make_event (author = "system" , text = "system preamble" ),
564+ _make_event (author = "agent" , text = "agent answer" ),
565+ ]
566+ session = _make_session (events = events )
567+
568+ result = await summarizer .create_session_summary (session )
569+
570+ assert result == "session summary"
571+ assert len (session .events ) == 3
572+ visible_events = [event for event in session .events if event .is_model_visible ()]
573+ assert len (visible_events ) == 1
574+ assert visible_events [0 ].is_summary_event ()
575+
410576 async def test_summary_no_update_on_failure (self ):
411577 model = _make_model_mock ()
412578
0 commit comments