Skip to content

Conversation

@misrasaurabh1
Copy link
Contributor

📄 18,798% (187.98x) speedup for AgentRunResult._set_output_tool_return in pydantic_ai_slim/pydantic_ai/agent.py

⏱️ Runtime : 8.31 milliseconds 44.0 microseconds (best of 21 runs)

📝 Explanation and details

Key optimization:

  • The deep copy of the whole message history (deepcopy(self._state.message_history)) was by far the slowest operation (99.3% of time).
  • The intent is to copy the list and its objects enough to mutate the last message's tool return part safely.
  • We optimize by.
    • Copying the message list shallowly, and only making a shallow copy of the last message and a shallow copy of its parts. This is much faster than recursively copying every nested attribute of every message.
    • We still keep copy-on-write semantics for the last message and the relevant parts subtree.
  • All other logic remains, but now is much faster due to a reduced amount of copying.

Notes:

  • We only deep-copy branches of the structure that will actually be mutated. This dramatically reduces overhead.
  • Defined behavior assumes ModelMessage is a dataclass or otherwise supports __class__(**__dict__) for shallow copying. If this can't be guaranteed, substitute with a more explicit copy (e.g., dataclasses.replace if it is a dataclass).
  • All output and error conditions remain identical.
  • The comments are preserved and extended for clarity where the code was changed.

Correctness verification report:

Test Status
⚙️ Existing Unit Tests 🔘 None Found
🌀 Generated Regression Tests 🔘 None Found
⏪ Replay Tests 2 Passed
🔎 Concolic Coverage Tests 🔘 None Found
📊 Tests Coverage 88.9%
⏪ Replay Tests and Runtime
Test File::Test Function Original ⏱️ Optimized ⏱️ Speedup

Codeflash

codeflash-ai bot and others added 2 commits July 12, 2025 09:37
Here's an optimized version of your code, focusing on the main bottleneck: `deepcopy(self._state.message_history)`.

**Optimization explanation:**
- `deepcopy` is extremely expensive (see profile: 99% of the time). Here, we only need to modify the last message's relevant tool part; the rest of `message_history` can be shared *unchanged*.
- We replace `deepcopy` with a shallow copy for `messages`. Then we make a **copy of the last message** and only copy its `parts` list. This isolates mutation to the last message and its parts while *reusing* all previous messages unmodified.
- This minimizes object allocations and recursive copying.

**Functionality remains identical** (the changed code only avoids unnecessary copying).



**Summary of improvements:**  
- No deep copy unless necessary (minimal required copying).  
- Lower memory churn and much faster.  
- Identical results and error handling.  
- Compatible with existing code and data class message structures.  

This will provide a **dramatic speedup**, especially when `message_history` is long.
@Kludex Kludex requested a review from Copilot July 15, 2025 08:31
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

A significant performance optimization for the AgentRunResult._set_output_tool_return method by replacing a full deep copy of the message history with a lazy copy that only deep copies the last message when needed.

  • Removed full deepcopy of message_history
  • Introduced shallow copy of list and deep copy of only the last message
  • Maintained original error conditions and semantics
Comments suppressed due to low confidence (3)

pydantic_ai_slim/pydantic_ai/agent.py:2105

  • The new lazy-copy logic in this method introduces multiple code paths (no-op vs. deep-copy-and-modify). Consider adding unit tests that verify both branches—ensuring the original message_history isn’t mutated when there’s no matching tool part and that only the last message is copied when a match is found.
        messages = self._state.message_history

pydantic_ai_slim/pydantic_ai/agent.py:2111

  • [nitpick] The variable copied_last could be renamed to copied_last_message for better clarity and readability.
                copied_last = deepcopy(last_message)

pydantic_ai_slim/pydantic_ai/agent.py:2105

  • [nitpick] The docstring still describes a full deep copy of the history but the implementation now uses a lazy-copy approach. Update the method’s docstring to reflect that only the last message is deep-copied when modified.
        messages = self._state.message_history

Copy link
Member

@samuelcolvin samuelcolvin left a comment

Choose a reason for hiding this comment

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

LGTM

@DouweM DouweM merged commit e6396cc into pydantic:main Jul 17, 2025
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants