Skip to content

Commit dca5aa3

Browse files
committed
Merge remote-tracking branch 'upstream/main' into feat/cube-code-executor
Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # trpc_agent_sdk/code_executors/_base_code_executor.py
2 parents 1df93b7 + 5558813 commit dca5aa3

16 files changed

Lines changed: 467 additions & 92 deletions

tests/agents/core/test_history_processor.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,18 @@ def test_invocation_mode_filters_by_id(self, invocation_context):
116116
assert len(events) == 1
117117
assert events[0].content.parts[0].text == "current"
118118

119+
def test_invocation_mode_includes_summary_events(self, invocation_context):
120+
proc = HistoryProcessor(timeline_filter_mode=TimelineFilterMode.INVOCATION)
121+
summary_event = _make_event("system", "Previous conversation summary", invocation_id="summary")
122+
summary_event.set_summary_event(True)
123+
current_event = _make_event("user", "current", invocation_id="inv-1")
124+
125+
events = proc.filter_events(invocation_context, [summary_event, current_event])
126+
127+
assert len(events) == 2
128+
assert events[0].is_summary_event()
129+
assert events[1].content.parts[0].text == "current"
130+
119131

120132
# ---------------------------------------------------------------------------
121133
# HistoryProcessor.filter_events - Branch filtering

tests/sessions/test_base_session_service.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,8 @@ def test_filter_by_num_recent_events(self):
170170
svc = ConcreteSessionService(session_config=config)
171171
session = _make_session()
172172
for i in range(10):
173-
session.events.append(_make_event(text=f"msg{i}"))
173+
author = "user" if i == 7 else "agent"
174+
session.events.append(_make_event(author=author, text=f"msg{i}"))
174175
svc.filter_events(session)
175176
assert len(session.events) == 10
176177
visible_events = [event for event in session.events if event.is_model_visible()]
@@ -185,7 +186,7 @@ def test_filter_by_event_ttl(self):
185186
old_event.timestamp = time.time() - 100
186187
session.events.append(old_event)
187188

188-
new_event = _make_event(text="new")
189+
new_event = _make_event(author="user", text="new")
189190
new_event.timestamp = time.time()
190191
session.events.append(new_event)
191192

@@ -215,6 +216,23 @@ def test_filter_ttl_removes_all_old(self):
215216
assert len(session.events) == 5
216217
assert all(not event.is_model_visible() for event in session.events)
217218

219+
def test_filter_by_num_recent_events_preserves_summary_anchor(self):
220+
config = SessionServiceConfig(num_recent_events=3)
221+
svc = ConcreteSessionService(session_config=config)
222+
session = _make_session()
223+
224+
summary_event = _make_event(author="system", text="summary")
225+
summary_event.set_summary_event(True)
226+
session.events.append(summary_event)
227+
for i in range(5):
228+
session.events.append(_make_event(text=f"agent{i}"))
229+
230+
svc.filter_events(session)
231+
232+
visible_events = [event for event in session.events if event.is_model_visible()]
233+
assert len(visible_events) == 1
234+
assert visible_events[0].is_summary_event()
235+
218236

219237
class TestBaseSessionServiceSetSummarizerManager:
220238
"""Test set_summarizer_manager method."""

tests/sessions/test_session.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from google.genai.types import Part
1111
from trpc_agent_sdk.events import Event
1212
from trpc_agent_sdk.sessions import Session
13+
from trpc_agent_sdk.sessions import is_summary_anchor
1314

1415

1516
class TestSession:
@@ -35,15 +36,16 @@ def test_add_event(self):
3536
assert session.events[0].author == "user"
3637
assert session.last_update_time == event.timestamp
3738

38-
def test_is_user_message(self):
39-
"""Test checking if an event is a user message."""
40-
session = Session(id="test-session", app_name="test-app", user_id="test-user", save_key="test-key")
41-
39+
def test_is_anchor_message(self):
40+
"""Test checking if an event can anchor visible conversation history."""
4241
user_event = Event(author="user", content=Content(parts=[Part.from_text(text="Hello")]))
4342
agent_event = Event(author="agent-1", content=Content(parts=[Part.from_text(text="Hi")]))
43+
summary_event = Event(author="system", content=Content(parts=[Part.from_text(text="Summary")]))
44+
summary_event.set_summary_event(True)
4445

45-
assert session._is_user_message(user_event) is True
46-
assert session._is_user_message(agent_event) is False
46+
assert is_summary_anchor(user_event) is True
47+
assert is_summary_anchor(agent_event) is False
48+
assert is_summary_anchor(summary_event) is True
4749

4850
def test_apply_event_filtering_no_config(self):
4951
"""Test event filtering with no configuration."""

tests/sessions/test_session_summarizer.py

Lines changed: 193 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)