Problem
Multiple frozen models contain dict[str, Any] fields with documented warnings:
# Note: The ``parameters_schema`` dict is shallowly immutable under the
# frozen model — reassignment is prevented but contents can still be
# mutated. Callers should treat it as read-only.
This appears on:
ToolDefinition.parameters_schema
ToolCall.arguments
ToolExecutionResult.metadata
- Various config models
The current approach is inconsistent:
BaseTool does copy.deepcopy on property access (expensive, see related issue)
ToolInvoker does dict(tool_call.arguments) (shallow copy — nested refs still shared)
- Most models just document "please don't mutate" and hope for the best
Pydantic's frozen=True is confirmed to be shallow — nested mutable objects can still be mutated in-place.
Options to Evaluate
| Approach |
Pros |
Cons |
| A. Document-only |
Zero overhead, honest |
No enforcement, bugs possible |
B. MappingProxyType at construction |
O(1), prevents top-level mutation |
Shallow — nested dicts still mutable |
| C. Recursive freeze utility |
True deep immutability |
Complex, perf overhead at construction |
| D. Deep-copy at boundary only |
Isolation where it matters |
Must identify all boundaries correctly |
Recommendation
Option D — deep-copy only at system boundaries (when passing data to tool execute(), when serializing for persistence), with MappingProxyType for the common read-only access path. This balances safety and performance.
Acceptance Criteria
Problem
Multiple frozen models contain
dict[str, Any]fields with documented warnings:This appears on:
ToolDefinition.parameters_schemaToolCall.argumentsToolExecutionResult.metadataThe current approach is inconsistent:
BaseTooldoescopy.deepcopyon property access (expensive, see related issue)ToolInvokerdoesdict(tool_call.arguments)(shallow copy — nested refs still shared)Pydantic's
frozen=Trueis confirmed to be shallow — nested mutable objects can still be mutated in-place.Options to Evaluate
MappingProxyTypeat constructionRecommendation
Option D — deep-copy only at system boundaries (when passing data to tool
execute(), when serializing for persistence), withMappingProxyTypefor the common read-only access path. This balances safety and performance.Acceptance Criteria
ToolInvokerboundary handling aligned with chosen strategy