Skip to content

Python: fix circular reference error in KernelArguments.dumps() during OTel diagnostics#13642

Open
MaxwellCalkin wants to merge 1 commit intomicrosoft:mainfrom
MaxwellCalkin:fix/kernel-arguments-circular-reference
Open

Python: fix circular reference error in KernelArguments.dumps() during OTel diagnostics#13642
MaxwellCalkin wants to merge 1 commit intomicrosoft:mainfrom
MaxwellCalkin:fix/kernel-arguments-circular-reference

Conversation

@MaxwellCalkin
Copy link

Motivation and Context

Fixes #13393

When running Process Framework steps with SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE=true, every function invocation fails with Circular reference detected. This happens because:

  1. KernelFunction.invoke() calls arguments.dumps() to serialize function arguments for OpenTelemetry span attributes
  2. Process step functions receive a KernelProcessStepContext argument (injected by find_input_channels() in step_utils.py)
  3. KernelProcessStepContext contains a step_message_channel field, which is the LocalStep instance itself
  4. LocalStep extends KernelBaseModel and contains Kernel, KernelFunction objects, output edges, and other complex runtime objects that form circular reference chains
  5. When dumps() calls model_dump() on the context, then passes the result to json.dumps(), the circular references cause ValueError: Circular reference detected

Description

Makes KernelArguments.dumps() resilient to circular references:

  • Object ID tracking: The custom default serializer now tracks id() of objects it has already visited, returning a safe placeholder string (<circular ref: ClassName>) instead of recursing into circular structures
  • Fallback for model_dump() failures: Wraps model_dump() in a try/except so that if a Pydantic model itself raises during serialization, the type name is used as a placeholder
  • Top-level ValueError catch: If json.dumps() still detects a circular reference (e.g., from nested dicts produced by model_dump()), falls back to per-key serialization where each un-serializable value is replaced with its type name

This is a defensive fix at the serialization boundary — it ensures OTel diagnostics never crash function invocations, regardless of argument complexity.

Contribution Checklist


Disclosure: This PR was authored by Claude Opus 4.6 (an AI). I'm applying for a role at Microsoft — see maxwellcalkin.com for background.

…g OTel diagnostics

When SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE
is enabled, KernelFunction.invoke() calls arguments.dumps() to serialize
function arguments for OpenTelemetry spans. If the arguments contain
objects with circular references (e.g., KernelProcessStepContext, whose
step_message_channel holds back-references to the process graph), this
causes json.dumps to raise 'Circular reference detected'.

The fix adds defensive handling in KernelArguments.dumps():
- Track seen object IDs to detect circular references in the custom
  default serializer
- Catch ValueError from json.dumps and fall back to per-key
  serialization, replacing un-serializable values with their type name

Fixes microsoft#13393
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

This PR hardens Python KernelArguments.dumps() so OpenTelemetry sensitive diagnostics do not crash function invocations when arguments contain circular references, as seen in Process Framework step contexts.

Changes:

  • Add circular reference handling and safer fallbacks inside KernelArguments.dumps().
  • Add unit tests covering basic serialization, Pydantic model serialization, and circular reference inputs.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
python/semantic_kernel/functions/kernel_arguments.py Adds circular reference detection, Pydantic model_dump() fallback, and a ValueError recovery path during JSON serialization.
python/tests/unit/functions/test_kernel_arguments.py Adds unit tests for KernelArguments.dumps() including a circular reference scenario.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

json.dumps(value, default=default)
safe_data[key] = value
except (ValueError, TypeError):
safe_data[key] = f"<{type(value).__name__}>"
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

The seen set is mutated as a side effect of calling json.dumps(value, default=default) in the fallback loop, but safe_data[key] stores the original value. When the final json.dumps(safe_data, default=default) runs, default() can see the same object again and incorrectly treat it as a circular reference (for example, a non circular BaseModel value in another key becomes <circular ref: ...>). Consider building safe_data from already-sanitized JSON-compatible values (for example, store the result of serializing each value once), or reset seen appropriately so values are not re-encoded with stale state.

Suggested change
safe_data[key] = f"<{type(value).__name__}>"
safe_data[key] = f"<{type(value).__name__}>"
# Reset seen so previous serialization attempts don't affect final encoding.
seen.clear()

Copilot uses AI. Check for mistakes.
Comment on lines +123 to +129
seen: set[int] = set()

def default(obj: Any) -> Any:
obj_id = id(obj)
if obj_id in seen:
return f"<circular ref: {type(obj).__name__}>"
seen.add(obj_id)
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

seen is used as a global "already visited" set, which means repeated references to the same object (even without a cycle) will be serialized as <circular ref: ...> on the second occurrence. If preserving data for shared references matters, consider tracking only the current recursion stack (cycle detection) rather than all previously seen objects, or sanitize containers recursively before calling json.dumps so you can pop from the stack when unwinding.

Copilot uses AI. Check for mistakes.
# This should not raise ValueError: Circular reference detected
result = kargs.dumps()
assert isinstance(result, str)
assert "data" in result
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

The circular-reference test only asserts that the returned string contains "data". This can pass even if the output is not valid JSON or if the fallback behavior changes unexpectedly. It would be more robust to json.loads(result) and assert the expected placeholder value for the circular input (for example, that parsed["data"] is a safe string placeholder rather than raising).

Suggested change
assert "data" in result
import json
parsed = json.loads(result)
# Ensure we produced valid JSON with a safe placeholder for the circular data
assert "data" in parsed
assert isinstance(parsed["data"], str)

Copilot uses AI. Check for mistakes.
Comment on lines +184 to +190
def test_kernel_arguments_dumps_basic():
"""Test basic dumps serialization."""
kargs = KernelArguments(name="test", value=42)
result = kargs.dumps()
import json

parsed = json.loads(result)
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

These tests import json inside the test body. In this test suite json is typically imported at module scope; moving the import to the top keeps imports consistent and avoids repeated imports across tests.

Copilot uses AI. Check for mistakes.
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.

Python: Bug: Python: Circular Reference Error when running Process samples with SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE=true

2 participants