From fca78f596d4e7bf04106cf9d5ade823809dde847 Mon Sep 17 00:00:00 2001 From: thejesh23 Date: Sat, 1 Aug 2026 02:21:29 -0700 Subject: [PATCH] Serialize tool results with Pydantic's JSON mode `_normalize_result`'s `default` hook called `model_dump()`, which uses the default `mode="python"`. That leaves `datetime`, `date`, `UUID`, `Decimal`, `Enum` and `set` fields as native Python objects, so `json.dumps` hands them straight back to the same hook, which does not recognise them and raises `TypeError`. The raise happens inside `wrapped_handler`'s try block, which converts any exception into the deliberately redacted failure result. A tool returning a perfectly ordinary model such as class Issue(BaseModel): id: uuid.UUID created_at: datetime.datetime therefore reports "Invoking this tool produced an error. Detailed information is not available." to the model on every call, with no indication to the author of what went wrong. Switching to `model_dump(mode="json")` makes Pydantic coerce each field to a JSON-native type before `json.dumps` sees it, which matches what the TypeScript SDK already does via `JSON.stringify` (JS `Date` implements `toJSON`). Models whose fields are all JSON primitives serialize exactly as before. --- python/copilot/tools.py | 6 +++++- python/test_tools.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/python/copilot/tools.py b/python/copilot/tools.py index 762b79c45..2de29584f 100644 --- a/python/copilot/tools.py +++ b/python/copilot/tools.py @@ -351,7 +351,11 @@ def _normalize_result(result: Any) -> ToolResult: # Everything else gets JSON-serialized (with Pydantic model support) def default(obj: Any) -> Any: if isinstance(obj, BaseModel): - return obj.model_dump() + # mode="json" coerces datetime/UUID/Decimal/Enum fields to JSON-native + # types. The default mode="python" leaves them as native objects, which + # json.dumps then passes back into this hook and we reject as + # unserializable -- turning an ordinary tool result into a failure. + return obj.model_dump(mode="json") raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable") try: diff --git a/python/test_tools.py b/python/test_tools.py index 646f17c03..fa8329cd1 100644 --- a/python/test_tools.py +++ b/python/test_tools.py @@ -1,6 +1,10 @@ """Unit tests for define_tool""" +import datetime +import decimal +import enum import json +import uuid import pytest from pydantic import BaseModel, ConfigDict, Field, field_validator @@ -389,6 +393,33 @@ class Item(BaseModel): assert parsed == [{"name": "a", "value": 1}, {"name": "b", "value": 2}] assert result.result_type == "success" + def test_pydantic_model_with_non_primitive_fields_is_serialized(self): + class IssueState(enum.StrEnum): + OPEN = "open" + CLOSED = "closed" + + class Issue(BaseModel): + id: uuid.UUID + created_at: datetime.datetime + price: decimal.Decimal + state: IssueState + + issue = Issue( + id=uuid.UUID("6b686a99-0000-4000-8000-000000000000"), + created_at=datetime.datetime(2026, 8, 1, 1, 49, 10), + price=decimal.Decimal("1.5"), + state=IssueState.OPEN, + ) + result = _normalize_result(issue) + parsed = json.loads(result.text_result_for_llm) + assert parsed == { + "id": "6b686a99-0000-4000-8000-000000000000", + "created_at": "2026-08-01T01:49:10", + "price": "1.5", + "state": "open", + } + assert result.result_type == "success" + def test_raises_for_unserializable_value(self): # Functions cannot be JSON serialized with pytest.raises(TypeError, match="Failed to serialize"):