Skip to content

RecursionError in deeply recursive discriminated union types (AST operator nodes) #753

@kraenhansen

Description

@kraenhansen

Summary

The Python SDK fails with RecursionError when importing types that contain deeply recursive discriminated unions. This is triggered by the Fern regeneration on branch fern-bot/2026-03-30T11-49Z (PR #752), which added 4 new arithmetic operator node types (add, sub, mul, div) to the ConvAI AST expression system. The CI run shows 90 type files failing with RecursionError.

Root Cause

The ConvAI agent workflow system models expressions as an AST (Abstract Syntax Tree). Every operator node type (and, or, equals, greater_than, etc.) has a children / left / right field whose type is a discriminated union that can be any other operator node type — including itself. This creates a fully connected graph of cross-references.

Before PR #752: ~10 operator types → ~100 cross-file references → Python handles it.

After PR #752: ~14 operator types → ~196 cross-file references → RecursionError at import time.

The problem is structural: Fern generates each discriminated union variant set as a separate file, which creates circular import chains. Python's forward reference resolution (update_forward_refs / model_rebuild) hits the recursion limit when the import graph becomes too deep.

Failing types (sample):

adhoc_agent_config_override_for_test_request_model  RecursionError: maximum recursion depth exceeded while calling a Python object
agent_workflow_request_model                         RecursionError: maximum recursion depth exceeded while calling a Python object
ast_addition_operator_node_input                     RecursionError: maximum recursion depth exceeded in __instancecheck__
ast_and_operator_node_input                          RecursionError: maximum recursion depth exceeded while calling a Python object
ast_conditional_operator_node_input_condition        RecursionError: maximum recursion depth exceeded
workflow_edge_model_input                            RecursionError: maximum recursion depth exceeded while calling a Python object
workflow_expression_condition_model_input            RecursionError: maximum recursion depth exceeded while calling a Python object
# ... 83 more

Prior Art

This exact problem was hit in October 2025. A workaround was developed in PR #658 on branch fern-support/fix-recursion-error (commits 7114c16, 9af0c47) but never merged because it lacked a sustainable mechanism.

The workaround consolidated all mutually recursive variant classes into two large files:

  • src/elevenlabs/types/ast_operators_input_consolidated.py (~4000 lines)
  • src/elevenlabs/types/ast_operators_output_consolidated.py (~4000 lines)

Each individual *_children_item.py / *_left.py / *_right.py file was replaced with a thin re-export stub. The trick: putting all definitions in one file with from __future__ import annotations at the top means Python never evaluates type annotations at class-definition time, breaking the cycle. update_forward_refs() then resolves everything at the end.

This workaround was not added to .fernignore, so the next regeneration wiped it out. It also quickly became stale as the OpenAPI schema evolved.

Affected Types (PR #752)

The following type families are involved in the recursive graph and need special handling:

AST operator node union types (each has children, left, or right fields that can be any operator):

  • AstAndOperatorNodeInput/Outputchildren field
  • AstOrOperatorNodeInput/Outputchildren field
  • AstEqualsOperatorNodeInput/Outputleft/right fields
  • AstGreaterThanOperatorNodeInput/Outputleft/right fields
  • AstGreaterThanOrEqualsOperatorNodeInput/Outputleft/right fields
  • AstLessThanOperatorNodeInput/Outputleft/right fields
  • AstLessThanOrEqualsOperatorNodeInput/Outputleft/right fields
  • AstNotEqualsOperatorNodeInput/Outputleft/right fields
  • AstConditionalOperatorNodeInput/Outputcondition/trueExpression/falseExpression fields
  • NEW: AstAdditionOperatorNodeInput/Outputleft/right fields
  • NEW: AstSubtractionOperatorNodeInput/Outputleft/right fields
  • NEW: AstMultiplicationOperatorNodeInput/Outputleft/right fields
  • NEW: AstDivisionOperatorNodeInput/Outputleft/right fields

Workflow condition types (hold expression fields):

  • WorkflowEdgeModelInput/OutputforwardCondition/backwardCondition fields
  • WorkflowExpressionConditionModelInput/Outputexpression field

Proposed Fix (for ElevenLabs to apply manually)

Apply the consolidated file approach from PR #658, sustainably:

  1. Create updated consolidated files that include all current variants (including ConditionalOperator added since Oct 2025) plus the 4 new arithmetic operators:

    • src/elevenlabs/types/ast_operators_input_consolidated.py
    • src/elevenlabs/types/ast_operators_output_consolidated.py
  2. Replace individual stub files — all *_children_item.py, *_left.py, *_right.py, and the new arithmetic operator files — with thin re-exports from the consolidated file.

  3. Add all modified/new files to .fernignore so future regenerations don't overwrite them. This is the critical step that was missing from PR fix(types): refactored type definitions to fix infinite recursion error #658.

  4. Update tests/test_recursive_models.py to instantiate the new arithmetic operator types.

Proposed Fix (for Fern to implement upstream)

The sustainable fix is for Fern's Python generator to detect mutually recursive discriminated union type graphs and consolidate them into a single module automatically, rather than generating one file per type. Specifically:

  1. Cycle detection: During codegen, detect strongly connected components (SCCs) in the type reference graph.
  2. Module consolidation: For each SCC with more than one node, emit all types in that SCC into a single Python module instead of separate files.
  3. Re-export stubs: Generate thin re-export files at the expected paths so that existing imports continue to work.
  4. from __future__ import annotations: Ensure the consolidated module uses deferred annotation evaluation (PEP 563) so class bodies with forward references are never evaluated eagerly.

This is the same pattern used by tools like datamodel-code-generator and hand-written Pydantic models for recursive schemas.

Repro

git clone https://github.com/elevenlabs/elevenlabs-python
cd elevenlabs-python
git checkout fern-bot/2026-03-30T11-49Z
poetry install
poetry run python -c "from elevenlabs.types import AstAndOperatorNodeInput"
# RecursionError: maximum recursion depth exceeded while calling a Python object

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions