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
71 changes: 69 additions & 2 deletions python/packages/core/agent_framework/_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ class _RunContext(TypedDict):
tokenizer: TokenizerProtocol | None
client_kwargs: Mapping[str, Any]
function_invocation_kwargs: Mapping[str, Any]
store_final_function_result_content: bool


# region Agent Protocol
Expand Down Expand Up @@ -940,6 +941,7 @@ async def _run_non_streaming() -> AgentResponse[Any]:
agent_name=ctx["agent_name"],
session=ctx["session"],
session_context=ctx["session_context"],
store_final_function_result_content=ctx["store_final_function_result_content"],
)
response_format = ctx["chat_options"].get("response_format")
if not (
Expand Down Expand Up @@ -989,8 +991,11 @@ async def _post_hook(response: AgentResponse) -> None:

# Run after_run providers (reverse order)
session_context = ctx["session_context"]
filtered_messages = self._filter_final_function_result_content(
response.messages, ctx["store_final_function_result_content"]
)
session_context._response = AgentResponse( # type: ignore[assignment]
messages=response.messages,
messages=filtered_messages,
response_id=response.response_id,
)
await self._run_after_providers(session=ctx["session"], context=session_context)
Expand Down Expand Up @@ -1109,6 +1114,7 @@ async def _prepare_run_context(
) -> _RunContext:
opts = dict(options) if options else {}
existing_additional_args: dict[str, Any] = opts.pop("additional_function_arguments", None) or {}
store_final_frc: bool = opts.pop("store_final_function_result_content", False)

Comment on lines 1115 to 1118
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

store_final_function_result_content is only read from per-run options (via opts.pop(...)) and does not consider Agent.default_options. If a user sets this flag as a default option, it will (1) be ignored for filtering (ctx value remains False) and (2) be forwarded down to client.get_response() via co, potentially breaking clients that reject unknown options. Consider deriving the effective flag from the merged options (run overrides defaults), then removing it from the options dict passed to the client.

Copilot uses AI. Check for mistakes.
# Get tools from options or named parameter (named param takes precedence)
tools_ = tools if tools is not None else opts.pop("tools", None)
Expand Down Expand Up @@ -1234,14 +1240,69 @@ async def _prepare_run_context(
"tokenizer": tokenizer or self.tokenizer,
"client_kwargs": effective_client_kwargs,
"function_invocation_kwargs": additional_function_arguments,
"store_final_function_result_content": store_final_frc,
}

@staticmethod
def _filter_final_function_result_content(
response_messages: list[Message],
store_final_function_result_content: bool,
) -> list[Message]:
"""Filter trailing function result messages from response messages.

Walks backward through the response messages, removing consecutive trailing
messages whose role is ``"tool"`` and whose content is entirely
``function_result`` type. Messages with mixed content are left unchanged.
The walk stops at the first message that does not match.

This aligns the behavior of chat history stored via a
:class:`BaseHistoryProvider` with the behavior of agents that store
chat history in the underlying AI service where the final function
result content is never stored.

Args:
response_messages: The response messages to filter.
store_final_function_result_content: When True, skip filtering
and return messages as-is.

Returns:
The filtered list of messages, or the original list if nothing was filtered.
"""
if store_final_function_result_content:
return response_messages

if not response_messages:
return response_messages

# Walk backward, removing trailing tool-role messages that contain only function_result.
first_kept_index = len(response_messages)
for i in range(len(response_messages) - 1, -1, -1):
message = response_messages[i]

if message.role != "tool":
break

all_function_result = len(message.contents) > 0 and all(
content.type == "function_result" for content in message.contents
)

if not all_function_result:
break

first_kept_index = i

if first_kept_index == len(response_messages):
return response_messages

return response_messages[:first_kept_index]

async def _finalize_response(
self,
response: ChatResponse,
agent_name: str,
session: AgentSession | None,
session_context: SessionContext,
store_final_function_result_content: bool = False,
) -> None:
"""Finalize response by setting author names and running after_run providers.

Expand All @@ -1250,6 +1311,8 @@ async def _finalize_response(
agent_name: The name of the agent to set as author.
session: The conversation session.
session_context: The invocation context.
store_final_function_result_content: When True, keep trailing
function result content in the stored history.
"""
# Ensure that the author name is set for each message in the response.
for message in response.messages:
Expand All @@ -1262,9 +1325,13 @@ async def _finalize_response(
if session and response.conversation_id and session.service_session_id != response.conversation_id:
session.service_session_id = response.conversation_id

filtered_messages = self._filter_final_function_result_content(
response.messages, store_final_function_result_content
)

# Set the response on the context for after_run providers
session_context._response = AgentResponse( # type: ignore[assignment]
messages=response.messages,
messages=filtered_messages,
response_id=response.response_id,
)

Expand Down
3 changes: 3 additions & 0 deletions python/packages/core/agent_framework/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3115,6 +3115,9 @@ class _ChatOptionsBase(TypedDict, total=False):
# System/instructions
instructions: str

# History storage
store_final_function_result_content: bool
Copy link
Member

Choose a reason for hiding this comment

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

is this common enough to have to be part of the core chat options? not sure I like that, it also is not really a service side setting, which is what is the target for the ChatOptions, so we might want to use client_kwargs for this


Comment on lines 3115 to +3120
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

store_final_function_result_content is included in ChatOptions, which is documented as “Common request settings for AI services”. This key is agent-side history-storage behavior (not a provider request parameter), so its presence in ChatOptions can mislead users/custom clients into passing it to client.get_response(). Consider clarifying in the comment/docs that this option is Agent-only and not forwarded to providers (or splitting non-provider run options into a separate TypedDict).

Copilot uses AI. Check for mistakes.

if TYPE_CHECKING:

Expand Down
Loading
Loading