Skip to content
Open
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
64 changes: 62 additions & 2 deletions python/packages/core/agent_framework/_compaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -1198,12 +1198,72 @@ def _tool_result_text(value: Any) -> str:
if text_parts:
return "\n".join(text_parts)
if isinstance(value, Mapping):
return json.dumps(cast(Mapping[str, object], value), ensure_ascii=False)
try:
return json.dumps(cast(Mapping[str, object], value), ensure_ascii=False, default=str)
except (TypeError, ValueError):
return str(cast(object, value))
return str(cast(object, value))


def _format_summary_content(content: Content) -> str:
"""Render one content item for the summarizer input transcript.

Tool calls and results are rendered with their name, arguments, result
text, and call id so the summarizer sees the tool trajectory instead of a
bare content type. Text contents are aggregated via ``Message.text``
instead. Returns an empty string when the item has no structured rendering
of its own, so callers can fall back to the legacy rendering.
"""
if content.type == "function_call":
arguments = _tool_result_text(content.arguments) if content.arguments is not None else ""
call = f"function_call {content.name or ''}({arguments})"
if content.call_id:
call += f" [call_id={content.call_id}]"
return call
if content.type == "function_result":
result_text = _tool_result_text(content.result) if content.result is not None else "no result"
if content.exception:
result_text = f"error({content.exception}): {result_text}"
call_id_suffix = f" [call_id={content.call_id}]" if content.call_id else ""
return f"function_result: {result_text}{call_id_suffix}"
if content.type == "mcp_server_tool_call":
arguments = _tool_result_text(content.arguments) if content.arguments is not None else ""
call = f"mcp_tool_call {content.tool_name or ''}({arguments})"
if content.call_id:
call += f" [call_id={content.call_id}]"
return call
if content.type == "mcp_server_tool_result":
result_text = _tool_result_text(content.output)
if content.exception:
result_text = f"error({content.exception}): {result_text}"
call_id_suffix = f" [call_id={content.call_id}]" if content.call_id else ""
return f"mcp_tool_result: {result_text}{call_id_suffix}"
if content.type in ("function_approval_request", "function_approval_response"):
nested_call = content.function_call
name = "" if nested_call is None else nested_call.name or nested_call.tool_name or ""
label = "approval_request" if content.type == "function_approval_request" else "approval_response"
rendered = f"{label}: {name} [id={content.id}]"
if content.type == "function_approval_response":
rendered += f" approved={content.approved}"
return rendered
return ""


def _format_summary_message(index: int, message: Message) -> str:
content_text = message.text
parts: list[str] = []
pending_text: list[str] = []
for content in message.contents:
rendered = _format_summary_content(content)
if rendered:
if pending_text:
parts.append(" ".join(pending_text))
pending_text = []
parts.append(rendered)
elif content.type == "text" and content.text:
pending_text.append(content.text)
if pending_text:
parts.append(" ".join(pending_text))
content_text = "; ".join(parts)
if not content_text:
content_text = ", ".join(content.type for content in message.contents)
return f"{index}. [{message.role}] {content_text}"
Expand Down
180 changes: 180 additions & 0 deletions python/packages/core/tests/core/test_compaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import logging
from datetime import date
from typing import Any

import pytest
Expand Down Expand Up @@ -36,6 +37,7 @@
included_token_count,
)
from agent_framework._compaction import (
_format_summary_message,
_select_summary_input_groups,
_serialize_message,
append_compaction_message,
Expand Down Expand Up @@ -972,6 +974,184 @@ def test_summary_input_selection_does_not_retokenize_selected_transcript() -> No
)


def test_format_summary_message_includes_function_call_details() -> None:
message = Message(
role="assistant",
contents=[Content.from_function_call(call_id="call_1", name="get_weather", arguments='{"city":"Seattle"}')],
)

rendered = _format_summary_message(1, message)

assert "get_weather" in rendered
assert '{"city":"Seattle"}' in rendered
assert "[call_id=call_1]" in rendered


def test_format_summary_message_includes_function_result_and_exception() -> None:
message = Message(
role="tool",
contents=[Content.from_function_result(call_id="call_1", result="42", exception="ValueError")],
)

rendered = _format_summary_message(2, message)

assert "function_result" in rendered
assert "42" in rendered
assert "error(ValueError)" in rendered
assert "[call_id=call_1]" in rendered


def test_format_summary_message_renders_function_result_without_call_id() -> None:
message = Message(
role="tool",
contents=[Content("function_result", call_id=None, result="done")],
)

rendered = _format_summary_message(3, message)

assert "done" in rendered
assert "call_id" not in rendered


def test_format_summary_message_combines_tool_calls_with_text() -> None:
message = Message(
role="assistant",
contents=[
"I'll check the weather.",
Content.from_function_call(call_id="call_1", name="get_weather", arguments='{"city":"Seattle"}'),
],
)

rendered = _format_summary_message(4, message)

assert "I'll check the weather." in rendered
assert "get_weather" in rendered


def test_format_summary_message_preserves_text_only_messages() -> None:
message = Message(role="user", contents=["hello world"])

rendered = _format_summary_message(5, message)

assert rendered == "5. [user] hello world"


def test_format_summary_message_includes_mcp_tool_details() -> None:
message = Message(
role="assistant",
contents=[
Content.from_mcp_server_tool_call(
call_id="mcp_1",
tool_name="search",
server_name="test_server",
arguments='{"query":"x"}',
),
Content.from_mcp_server_tool_result(
call_id="mcp_1",
output=[Content.from_text("found")],
),
],
)

rendered = _format_summary_message(6, message)

assert "search" in rendered
assert '{"query":"x"}' in rendered
assert "[call_id=mcp_1]" in rendered
assert "found" in rendered


def test_format_summary_message_includes_approval_request() -> None:
message = Message(
role="assistant",
contents=[
Content.from_function_approval_request(
id="approval_1",
function_call=Content.from_function_call(
call_id="call_1", name="send_email", arguments='{"to":"a@b.c"}'
),
)
],
)

rendered = _format_summary_message(7, message)

assert "approval_request" in rendered
assert "send_email" in rendered
assert "[id=approval_1]" in rendered


def test_format_summary_message_includes_approval_response() -> None:
message = Message(
role="assistant",
contents=[
Content.from_function_approval_response(
approved=True,
id="approval_1",
function_call=Content.from_function_call(
call_id="call_1", name="send_email", arguments='{"to":"a@b.c"}'
),
)
],
)

rendered = _format_summary_message(8, message)

assert "approval_response" in rendered
assert "approved=True" in rendered


def test_format_summary_message_stringifies_non_json_mcp_result_without_crash() -> None:
message = Message(
role="tool",
contents=[Content("mcp_server_tool_result", call_id="mcp_1", output={"when": date(2026, 1, 1)})],
)

rendered = _format_summary_message(9, message)

assert "2026" in rendered
assert "[call_id=mcp_1]" in rendered


def test_format_summary_message_preserves_time_order_for_mixed_contents() -> None:
message = Message(
role="assistant",
contents=[
"I'll check the weather.",
Content.from_function_call(call_id="call_1", name="get_weather", arguments='{"city":"Seattle"}'),
"Please wait.",
],
)

rendered = _format_summary_message(10, message)

assert rendered.index("I'll check the weather.") < rendered.index("function_call")
assert rendered.index("function_call") < rendered.index("Please wait.")


def test_format_summary_message_uses_tool_name_for_mcp_approval() -> None:
message = Message(
role="assistant",
contents=[
Content.from_function_approval_request(
id="approval_mcp_1",
function_call=Content.from_mcp_server_tool_call(
call_id="mcp_1",
tool_name="search",
server_name="test_server",
arguments='{"query":"x"}',
),
)
],
)

rendered = _format_summary_message(11, message)

assert "approval_request" in rendered
assert "search" in rendered
assert "[id=approval_mcp_1]" in rendered


async def test_summarization_strategy_returns_false_when_summary_generation_fails(
caplog: Any,
) -> None:
Expand Down
Loading