Skip to content

Conversation

@ruskaruma
Copy link
Contributor

Summary

Implements OpenAIResponsesCompactionSession to add seamless support for the newly added responses.compact API, matching the TypeScript SDK implementation (openai-agents-js#760).

Core Implementation

  • OpenAIResponsesCompactionSession: Session decorator that automatically compacts conversation history when it grows too large
  • CompactionItem: New item type to handle encrypted compaction content
  • OpenAIResponsesCompactionAwareSession: Protocol extension for compaction-aware sessions
  • is_openai_responses_compaction_aware_session(): Type guard for runtime checks

Key Features

  • Configurable should_trigger_compaction hook for custom compaction logic
  • Default threshold of 10 candidate items (excludes user messages and existing compaction items)
  • Model validation to ensure only OpenAI Responses API–compatible models are used
  • Automatic runner integration, compaction runs after each turn if threshold is met
  • Chat completions guardrail to prevent unsupported usage

Usage Example

from agents import Agent, MemorySession, OpenAIResponsesCompactionSession, Runner

session = OpenAIResponsesCompactionSession(
    session_id="my-session",
    underlying_session=MemorySession(),
)

result = await Runner.run(agent, "Hello!", session=session)

Test plan

  • Added 17 new tests in tests/memory/test_openai_responses_compaction_session.py

Issue number

Closes #2206

Checks

  • I've added new tests (if relevant)
  • I've added/updated the relevant documentation
  • I've run make lint and make format
  • I've made sure tests pass

@ruskaruma ruskaruma force-pushed the feature/compaction-support branch from c6d62d4 to cb496bf Compare December 23, 2025 11:16
@ruskaruma
Copy link
Contributor Author

Greetings @seratch and @ihower, this PR implements the core responses.compact support, aligned with the TypeScript SDK implementation (#760).

Following the TS SDK approach, a few related enhancements could either be handled in follow-up PRs or incorporated here, depending on your preference:

  • Usage tracking integration (similar to #785)
  • Documentation updates (similar to #784)

I’d appreciate your feedback on whether this aligns with the intended design for the issue, and I’m happy to adjust the implementation as needed.

@seratch seratch added this to the 0.7.x milestone Dec 23, 2025
@seratch
Copy link
Member

seratch commented Dec 23, 2025

Thanks for sending this. This looks like a good port of the TS implementation I did. I will review the details early next year.

@seratch seratch changed the title Add responses.compact: auto-compact long conversations (#2206) feat: #2206 Add responses.compact: auto-compact long conversations Dec 23, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Jan 3, 2026

This PR is stale because it has been open for 10 days with no activity.

@github-actions github-actions bot added the stale label Jan 3, 2026
@seratch seratch removed the stale label Jan 5, 2026
Copy link
Member

@seratch seratch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you again for working on this!

I've checked the code changes and found a few changes would be necessary:

diff --git a/examples/memory/compaction_session_example.py b/examples/memory/compaction_session_example.py
index c84822e3..cc1f6cf5 100644
--- a/examples/memory/compaction_session_example.py
+++ b/examples/memory/compaction_session_example.py
@@ -50,7 +50,8 @@ async def main():
     print("=== Final Session State ===")
     print(f"Total items: {len(items)}")
     for item in items:
-        item_type = item.get("type", "unknown")
+        # Some inputs are stored as easy messages (only `role` and `content`).
+        item_type = item.get("type") or ("message" if "role" in item else "unknown")
         if item_type == "compaction":
             print("  - compaction (encrypted content)")
         elif item_type == "message":
diff --git a/src/agents/memory/openai_responses_compaction_session.py b/src/agents/memory/openai_responses_compaction_session.py
index e23c5909..95b9a61d 100644
--- a/src/agents/memory/openai_responses_compaction_session.py
+++ b/src/agents/memory/openai_responses_compaction_session.py
@@ -29,12 +29,19 @@ def select_compaction_candidate_items(
 
     Excludes user messages and compaction items.
     """
+
+    def _is_user_message(item: TResponseInputItem) -> bool:
+        if not isinstance(item, dict):
+            return False
+        if item.get("type") == "message":
+            return item.get("role") == "user"
+        return item.get("role") == "user" and "content" in item
+
     return [
         item
         for item in items
         if not (
-            (item.get("type") == "message" and item.get("role") == "user")
-            or item.get("type") == "compaction"
+            _is_user_message(item) or (isinstance(item, dict) and item.get("type") == "compaction")
         )
     ]
 
@@ -160,7 +167,11 @@ class OpenAIResponsesCompactionSession(SessionABC, OpenAIResponsesCompactionAwar
                 if isinstance(item, dict):
                     output_items.append(item)
                 else:
-                    output_items.append(item.model_dump(exclude_unset=True))  # type: ignore
+                    # Suppress Pydantic literal warnings: responses.compact can return
+                    # user-style input_text content inside ResponseOutputMessage.
+                    output_items.append(
+                        item.model_dump(exclude_unset=True, warnings=False)  # type: ignore
+                    )
 
         if output_items:
             await self.underlying_session.add_items(output_items)
diff --git a/tests/memory/test_openai_responses_compaction_session.py b/tests/memory/test_openai_responses_compaction_session.py
index 204dbcb1..0b528701 100644
--- a/tests/memory/test_openai_responses_compaction_session.py
+++ b/tests/memory/test_openai_responses_compaction_session.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import warnings as warnings_module
 from typing import cast
 from unittest.mock import AsyncMock, MagicMock
 
@@ -62,6 +63,15 @@ class TestSelectCompactionCandidateItems:
         assert len(result) == 1
         assert result[0].get("type") == "message"
 
+    def test_excludes_easy_user_messages_without_type(self) -> None:
+        items: list[TResponseInputItem] = [
+            cast(TResponseInputItem, {"content": "hi", "role": "user"}),
+            cast(TResponseInputItem, {"type": "message", "role": "assistant", "content": "hello"}),
+        ]
+        result = select_compaction_candidate_items(items)
+        assert len(result) == 1
+        assert result[0].get("role") == "assistant"
+
 
 class TestOpenAIResponsesCompactionSession:
     def create_mock_session(self) -> MagicMock:
@@ -205,6 +215,43 @@ class TestOpenAIResponsesCompactionSession:
 
         mock_client.responses.compact.assert_called_once()
 
+    @pytest.mark.asyncio
+    async def test_run_compaction_suppresses_model_dump_warnings(self) -> None:
+        mock_session = self.create_mock_session()
+        mock_session.get_items.return_value = [
+            cast(TResponseInputItem, {"type": "message", "role": "assistant", "content": "hi"})
+            for _ in range(DEFAULT_COMPACTION_THRESHOLD)
+        ]
+
+        class WarningModel:
+            def __init__(self) -> None:
+                self.received_warnings_arg: bool | None = None
+
+            def model_dump(self, *, exclude_unset: bool, warnings: bool | None = None) -> dict:
+                self.received_warnings_arg = warnings
+                if warnings:
+                    warnings_module.warn("unexpected warning", stacklevel=2)
+                return {"type": "message", "role": "assistant", "content": "ok"}
+
+        warning_model = WarningModel()
+        mock_compact_response = MagicMock()
+        mock_compact_response.output = [warning_model]
+
+        mock_client = MagicMock()
+        mock_client.responses.compact = AsyncMock(return_value=mock_compact_response)
+
+        session = OpenAIResponsesCompactionSession(
+            session_id="test",
+            underlying_session=mock_session,
+            client=mock_client,
+        )
+
+        with warnings_module.catch_warnings():
+            warnings_module.simplefilter("error")
+            await session.run_compaction({"response_id": "resp-123"})
+
+        assert warning_model.received_warnings_arg is False
+
 
 class TestTypeGuard:
     def test_is_compaction_aware_session_true(self) -> None:

@seratch seratch modified the milestones: 0.7.x, 0.6.x Jan 8, 2026
@ruskaruma
Copy link
Contributor Author

Greetings @seratch, I have applied all requested changes. Apologies for the failed CI runs earlier, I have fixed them now. This should complete the work here

@ruskaruma ruskaruma requested a review from seratch January 8, 2026 23:32
@seratch
Copy link
Member

seratch commented Jan 8, 2026

@codex review again

@chatgpt-codex-connector
Copy link

Codex Review: Didn't find any major issues. You're on a roll.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@seratch seratch merged commit 09443fd into openai:main Jan 9, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Add responses.compact-wired session

2 participants