Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions libs/cli/deepagents_cli/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,21 @@ def create_cli_agent(
routes={},
)

from deepagents.graph import resolve_model

model = resolve_model(model)

from deepagents.middleware.summarization import (
SummarizationToolMiddleware,
create_summarization_middleware,
)

agent_middleware.append(
SummarizationToolMiddleware(
create_summarization_middleware(model, composite_backend)
)
)

# Create the agent
# Use provided checkpointer or fallback to InMemorySaver
final_checkpointer = checkpointer if checkpointer is not None else InMemorySaver()
Expand Down
44 changes: 42 additions & 2 deletions libs/cli/deepagents_cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,41 @@ def _format_token_count(count: int) -> str:
return str(count)


def _format_compact_limit(
keep: tuple[str, int | float], context_limit: int | None
) -> str:
"""Format compact retention settings into a human-readable limit string.

Args:
keep: Retention policy tuple from summarization defaults.
context_limit: Model context limit when available.

Returns:
A short display string describing the compact retention limit.
"""
keep_type, keep_value = keep

if keep_type == "messages":
count = int(keep_value)
noun = "message" if count == 1 else "messages"
return f"last {count} {noun}"

if keep_type == "tokens":
return f"{_format_token_count(int(keep_value))} tokens"

if keep_type == "fraction":
percent = float(keep_value) * 100
if context_limit is not None:
token_limit = max(1, int(context_limit * float(keep_value)))
return (
f"{_format_token_count(token_limit)} tokens "
f"({percent:.0f}% of {_format_token_count(context_limit)})"
)
return f"{percent:.0f}% of context window"

return "current retention threshold"


def _write_iterm_escape(sequence: str) -> None:
"""Write an iTerm2 escape sequence to stderr.

Expand Down Expand Up @@ -1327,7 +1362,7 @@ async def _handle_compact(self) -> None:
Compaction is a no-op when the conversation's total token count is
within the `keep` budget (by default 10% of the model's
`max_input_tokens`). Until that threshold is exceeded the user sees
"Nothing to compact yet".
"Nothing to compact yet" plus the active compact limit.
"""
if not self._agent or not self._lc_thread_id or not self._backend:
await self._mount_message(
Expand Down Expand Up @@ -1396,12 +1431,17 @@ async def _handle_compact(self) -> None:
effective = middleware._apply_event_to_messages(messages, event)

cutoff = middleware._determine_cutoff_index(effective)
compact_limit = _format_compact_limit(
defaults["keep"],
settings.model_context_limit,
)

if cutoff == 0:
await self._mount_message(
AppMessage(
"Nothing to compact yet"
" \u2014 conversation is within the token budget"
" \u2014 conversation is within the compact limit "
f"({compact_limit})"
)
)
return
Expand Down
30 changes: 30 additions & 0 deletions libs/cli/tests/unit_tests/test_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
from typing import TYPE_CHECKING, Any, cast
from unittest.mock import Mock, patch

from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
from langchain_core.messages import AIMessage

if TYPE_CHECKING:
from langchain.agents.middleware.types import AgentState
from langchain.messages import ToolCall
Expand All @@ -24,6 +27,13 @@
from deepagents_cli.config import Settings, get_glyphs


def _make_fake_chat_model() -> GenericFakeChatModel:
"""Create a fake chat model compatible with summarization middleware."""
model = GenericFakeChatModel(messages=iter([AIMessage(content="ok")]))
model.profile = {"max_input_tokens": 200000}
return model


def test_format_write_file_description_create_new_file(tmp_path: Path) -> None:
"""Test write_file description for creating a new file."""
new_file = tmp_path / "new_file.py"
Expand Down Expand Up @@ -497,11 +507,16 @@ def __init__(self, **kwargs: Any) -> None:
mock_agent = Mock()
mock_agent.with_config.return_value = mock_agent

fake_model = _make_fake_chat_model()
with (
patch("deepagents_cli.agent.settings", mock_settings),
patch("deepagents_cli.agent.SkillsMiddleware", FakeSkillsMiddleware),
patch("deepagents_cli.agent.MemoryMiddleware"),
patch("deepagents_cli.agent.create_deep_agent", return_value=mock_agent),
patch(
"deepagents.graph.init_chat_model",
return_value=fake_model,
),
):
create_cli_agent(
model="fake-model",
Expand Down Expand Up @@ -565,6 +580,7 @@ def __init__(self, **kwargs: Any) -> None:
mock_agent = Mock()
mock_agent.with_config.return_value = mock_agent

fake_model = _make_fake_chat_model()
with (
patch("deepagents_cli.agent.settings", mock_settings),
patch("deepagents_cli.agent.SkillsMiddleware"),
Expand All @@ -574,6 +590,10 @@ def __init__(self, **kwargs: Any) -> None:
"deepagents_cli.agent.create_deep_agent",
return_value=mock_agent,
),
patch(
"deepagents.graph.init_chat_model",
return_value=fake_model,
),
):
create_cli_agent(
model="fake-model",
Expand Down Expand Up @@ -626,6 +646,7 @@ def __init__(self, **kwargs: Any) -> None:
mock_agent = Mock()
mock_agent.with_config.return_value = mock_agent

fake_model = _make_fake_chat_model()
with (
patch("deepagents_cli.agent.settings", mock_settings),
patch("deepagents_cli.agent.SkillsMiddleware"),
Expand All @@ -635,6 +656,10 @@ def __init__(self, **kwargs: Any) -> None:
"deepagents_cli.agent.create_deep_agent",
return_value=mock_agent,
),
patch(
"deepagents.graph.init_chat_model",
return_value=fake_model,
),
):
create_cli_agent(
model="fake-model",
Expand Down Expand Up @@ -690,12 +715,17 @@ def capture_create_agent(**kwargs: Any) -> Mock:
agent.with_config.return_value = agent
return agent

fake_model = _make_fake_chat_model()
with (
patch("deepagents_cli.agent.settings", mock_settings),
patch(
"deepagents_cli.agent.create_deep_agent",
side_effect=capture_create_agent,
),
patch(
"deepagents.graph.init_chat_model",
return_value=fake_model,
),
):
create_cli_agent(
model="fake-model",
Expand Down
42 changes: 37 additions & 5 deletions libs/cli/tests/unit_tests/test_compact.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

import pytest

from deepagents_cli.app import DeepAgentsApp, _format_token_count
from deepagents_cli.app import DeepAgentsApp, _format_compact_limit, _format_token_count
from deepagents_cli.config import settings
from deepagents_cli.widgets.autocomplete import SLASH_COMMANDS
from deepagents_cli.widgets.messages import AppMessage, ErrorMessage

Expand Down Expand Up @@ -164,12 +165,18 @@ async def test_cutoff_zero_shows_not_enough(self) -> None:
await pilot.pause()
_setup_compact_app(app, n_messages=3)

with _mock_middleware(cutoff=0):
with (
_mock_middleware(cutoff=0),
patch.object(settings, "model_context_limit", 200_000),
):
await app._handle_compact()
await pilot.pause()

msgs = app.query(AppMessage)
assert any("Nothing to compact yet" in str(w._content) for w in msgs)
assert any(
"compact limit (20.0K tokens (10% of 200.0K))" in str(w._content)
for w in msgs
)

@pytest.mark.asyncio
async def test_empty_state_shows_error(self) -> None:
Expand Down Expand Up @@ -389,12 +396,18 @@ async def test_cutoff_zero_does_not_update_state(self) -> None:
await pilot.pause()
_setup_compact_app(app, n_messages=6)

with _mock_middleware(cutoff=0):
with (
_mock_middleware(cutoff=0),
patch.object(settings, "model_context_limit", 200_000),
):
await app._handle_compact()
await pilot.pause()

msgs = app.query(AppMessage)
assert any("Nothing to compact yet" in str(w._content) for w in msgs)
assert any(
"compact limit (20.0K tokens (10% of 200.0K))" in str(w._content)
for w in msgs
)
app._agent.aupdate_state.assert_not_called() # type: ignore[union-attr]

@pytest.mark.asyncio
Expand Down Expand Up @@ -913,3 +926,22 @@ def test_millions(self) -> None:

def test_above_million(self) -> None:
assert _format_token_count(2_500_000) == "2.5M"


class TestFormatCompactLimit:
"""Test the _format_compact_limit helper function."""

def test_format_messages_limit(self) -> None:
assert _format_compact_limit(("messages", 6), None) == "last 6 messages"

def test_format_tokens_limit(self) -> None:
assert _format_compact_limit(("tokens", 12_345), None) == "12.3K tokens"

def test_format_fraction_limit_with_context(self) -> None:
assert (
_format_compact_limit(("fraction", 0.1), 200_000)
== "20.0K tokens (10% of 200.0K)"
)

def test_format_fraction_limit_without_context(self) -> None:
assert _format_compact_limit(("fraction", 0.1), None) == "10% of context window"
Loading
Loading