Skip to content
Draft
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
49 changes: 48 additions & 1 deletion src/agents/agent_tool_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,21 @@ def _describe_json_schema_field(
if not isinstance(field_schema, dict):
return None

if any(key in field_schema for key in ("properties", "items", "oneOf", "anyOf", "allOf")):
if any(key in field_schema for key in ("properties", "items", "oneOf", "allOf")):
return None

description = _read_schema_description(field_schema)

# Pydantic renders `T | None` as `anyOf: [{type: T}, {type: "null"}]`.
# Treat that exact shape as the same as `type: ["T", "null"]` so optional
# simple fields still appear in the schema summary.
any_of = field_schema.get("anyOf")
if any_of is not None:
nullable_label = _describe_nullable_anyof(any_of)
if nullable_label is None:
return None
return _SchemaFieldDescription(type=nullable_label, description=description)

raw_type = field_schema.get("type")

if isinstance(raw_type, list):
Expand Down Expand Up @@ -245,6 +256,42 @@ def _describe_json_schema_field(
return None


def _describe_nullable_anyof(any_of: Any) -> str | None:
"""Render a 2-branch `anyOf` of a simple type and `null` as `T | null`.

Also handles `Optional[Literal[...]]`, which Pydantic emits as an `enum`/`const`
branch (possibly with `type: "string"`) plus a `null` branch.
"""
if not isinstance(any_of, list) or len(any_of) != 2:
return None
base_label: str | None = None
has_null = False
for entry in any_of:
if not isinstance(entry, dict):
return None
entry_type = entry.get("type")
if entry_type == "null":
has_null = True
continue
if base_label is not None:
return None
# Prefer `enum`/`const` over a bare `type` so `Optional[Literal[...]]`
# surfaces the allowed values rather than just e.g. `string | null`.
if isinstance(entry.get("enum"), list):
base_label = _format_enum_label(entry.get("enum"))
continue
if "const" in entry:
base_label = _format_literal_label(entry)
continue
if entry_type in _SIMPLE_JSON_SCHEMA_TYPES:
base_label = cast(str, entry_type)
continue
return None
if base_label is None or not has_null:
return None
return f"{base_label} | null"


def _read_schema_description(value: Any) -> str | None:
if not isinstance(value, dict):
return None
Expand Down
65 changes: 65 additions & 0 deletions tests/test_agent_tool_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,68 @@ def test_private_schema_helper_edge_cases() -> None:
assert _format_enum_label([]) == "enum"
assert "..." in _format_enum_label([1, 2, 3, 4, 5, 6])
assert _format_literal_label({}) == "literal"


def test_schema_summary_handles_pydantic_optional_anyof() -> None:
# Pydantic emits `T | None` as anyOf:[{type:T},{type:"null"}]; without
# support for that shape, a single Optional field nukes the whole summary.
schema = {
"type": "object",
"properties": {
"count": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "Optional count.",
},
"name": {"type": "string", "description": "A name."},
},
"required": ["name"],
}
summary = _build_schema_summary(schema)
assert summary is not None
assert "- count (integer | null, optional) - Optional count." in summary
assert "- name (string, required) - A name." in summary

# Non-nullable anyOf shapes (e.g. union of two simple types, or anyOf with
# nested objects) should still be rejected.
assert (
_build_schema_summary(
{
"type": "object",
"description": "x",
"properties": {
"u": {"anyOf": [{"type": "integer"}, {"type": "string"}]},
},
}
)
is None
)


def test_schema_summary_preserves_enum_const_in_nullable_anyof() -> None:
# Pydantic renders `Optional[Literal["a", "b"]]` as an anyOf branch carrying
# both an `enum` (with `type: "string"`) and a `null` branch. Surface the
# allowed values rather than collapsing to `string | null`.
schema = {
"type": "object",
"properties": {
"color": {
"anyOf": [
{"enum": ["red", "blue"], "type": "string"},
{"type": "null"},
],
"description": "Pick a color.",
},
"tag": {
"anyOf": [
{"const": "x", "type": "string"},
{"type": "null"},
],
"description": "A tag.",
},
},
"required": ["color", "tag"],
}
summary = _build_schema_summary(schema)
assert summary is not None
assert '- color (enum("red" | "blue") | null, required) - Pick a color.' in summary
assert '- tag (literal("x") | null, required) - A tag.' in summary