Skip to content
Open
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a17a8d2
fix: Detect and handle circular `$ref` in `_dereference_schema`
foivos-all Dec 9, 2025
17965d1
Merge branch 'main' into fix/handle-circular-refs
foivos-all Dec 9, 2025
81c75c9
Update src/google/adk/tools/_gemini_schema_util.py
foivos-all Dec 9, 2025
6721fbd
Merge branch 'main' into fix/handle-circular-refs
foivos-all Dec 9, 2025
2e40cb0
Merge branch 'main' into fix/handle-circular-refs
foivos-all Dec 9, 2025
7c1e6ce
Merge branch 'main' into fix/handle-circular-refs
foivos-all Dec 10, 2025
b029f36
Merge branch 'main' into fix/handle-circular-refs
foivos-all Dec 10, 2025
58aa121
Merge branch 'main' into fix/handle-circular-refs
foivos-all Dec 11, 2025
d104412
fix: executed `pyink`
foivos-all Dec 12, 2025
8f1782f
Merge branch 'main' into fix/handle-circular-refs
foivos-all Dec 12, 2025
6fda596
Merge branch 'main' into fix/handle-circular-refs
foivos-all Dec 12, 2025
813cfd6
Merge branch 'main' into fix/handle-circular-refs
foivos-all Dec 12, 2025
9c8e2f2
Merge branch 'main' into fix/handle-circular-refs
foivos-all Dec 12, 2025
b689025
Merge branch 'main' into fix/handle-circular-refs
foivos-all Dec 16, 2025
edde80d
Merge branch 'main' into fix/handle-circular-refs
foivos-all Dec 17, 2025
cedc98e
Merge branch 'main' into fix/handle-circular-refs
foivos-all Dec 18, 2025
17276dc
Merge branch 'main' into fix/handle-circular-refs
foivos-all Dec 18, 2025
da827fb
Merge branch 'main' into fix/handle-circular-refs
foivos-all Dec 18, 2025
3b20062
Merge branch 'main' into fix/handle-circular-refs
foivos-all Dec 22, 2025
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
74 changes: 66 additions & 8 deletions src/google/adk/tools/_gemini_schema_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,47 @@ def _dereference_schema(schema: dict[str, Any]) -> dict[str, Any]:
"""Resolves $ref pointers in a JSON schema."""

defs = schema.get("$defs", {})
# Track references currently being resolved to detect circular dependencies.
resolving = set()

def _resolve_refs(sub_schema: Any) -> Any:
def _resolve_json_pointer(ref_path: str, root: dict) -> Any:
"""Resolves a JSON Pointer reference path."""
if not ref_path.startswith("#/"):
return None

# Split the path into parts, skipping the leading "#/".
parts = ref_path[2:].split("/")
current = root

# Traverse the schema following the path.
for part in parts:
if not isinstance(current, dict):
return None
current = current.get(part)
if current is None:
return None

return current

def _resolve_refs(sub_schema: Any, path: str = "#") -> Any:
if isinstance(sub_schema, dict):
if "$ref" in sub_schema:
ref_key = sub_schema["$ref"].split("/")[-1]
ref = sub_schema["$ref"]

# Detect circular references by checking if we're already resolving
# this reference in the current call stack.
if ref in resolving:
# Return a placeholder schema to break the cycle.
return {
"type": "object",
"description": f"Circular reference to {ref}",
}

# Mark this reference as being resolved.
resolving.add(ref)

# Try to resolve as a $defs-style reference first.
ref_key = ref.split("/")[-1]
if ref_key in defs:
# Found the reference, replace it with the definition.
resolved = defs[ref_key].copy()
Expand All @@ -91,16 +127,38 @@ def _resolve_refs(sub_schema: Any) -> Any:
del sub_schema_copy["$ref"]
resolved.update(sub_schema_copy)
# Recursively resolve refs in the newly inserted part.
return _resolve_refs(resolved)
else:
# Reference not found, return as is.
return sub_schema
result = _resolve_refs(resolved, ref)
# Done resolving this reference, remove from tracking set.
resolving.discard(ref)
return result

# Try to resolve as a JSON Pointer reference.
resolved = _resolve_json_pointer(ref, schema)
if resolved is not None:
# Copy the resolved schema to avoid modifying the original.
resolved_copy = (
resolved.copy() if isinstance(resolved, dict) else resolved
)
# Recursively resolve refs in the resolved schema.
result = _resolve_refs(resolved_copy, ref)
resolving.discard(ref)
return result

# Reference not found in either $defs or as a JSON Pointer, return as is.
resolving.discard(ref)
return sub_schema
else:
# No $ref, so traverse deeper into the dictionary.
return {key: _resolve_refs(value) for key, value in sub_schema.items()}
return {
key: _resolve_refs(value, f"{path}/{key}")
for key, value in sub_schema.items()
}
elif isinstance(sub_schema, list):
# Traverse into lists.
return [_resolve_refs(item) for item in sub_schema]
return [
_resolve_refs(item, f"{path}[{i}]")
for i, item in enumerate(sub_schema)
]
else:
# Not a dict or list, return as is.
return sub_schema
Expand Down