Skip to content

Experimental recursive programs

Treelang schema version 2 is an opt-in preview. It supports declared user functions, lexical parameters, user calls, external tool calls, literals, and lazy conditionals. Version 1 remains the current root API and the default format produced by OpenAIArborist.

To opt into model generation, select schema version 2 and provide mandatory runtime limits before using WALK:

from treelang import ExecutionLimits
from treelang.ai.arborist import ArboristConfig, OpenAIArborist

arborist = OpenAIArborist(
    model="gpt-4o",
    provider=provider,
    config=ArboristConfig(model="gpt-4o", schema_version="2.0"),
    execution_limits=ExecutionLimits(
        max_call_depth=100,
        max_nodes=10_000,
        max_tool_calls=1_000,
        timeout_seconds=30,
    ),
)
response = await arborist.eval("Calculate 10 factorial recursively.")

TREE mode can return a validated v2 program without execution limits, allowing an application to inspect it before deciding whether and how to run it. V2 generation uses its own schema, rules, and recursive examples. Invalid model responses enter the configured validated-repair loop.

The deterministic direct- and mutual-recursion benchmark can be reproduced with:

uv run python evaluation/eval.py \
  --dataset evaluation/data/v2/offline-recursion.json \
  --baseline evaluation/baselines/v2/offline-recursion.json \
  --tolerances evaluation/baselines/v2/tolerances.json

Validate a version 2 program before executing it:

from treelang import ExecutionLimits
from treelang.trees.execution_v2 import execute_v2
from treelang.trees.schemas.v2 import AST

program = AST.model_validate(
    {
        "type": "program",
        "schema_version": "2.0",
        "definitions": [
            {
                "type": "function_definition",
                "name": "countdown",
                "params": ["n"],
                "body": {
                    "type": "conditional",
                    "condition": {
                        "type": "tool_call",
                        "tool": "less_than_or_equal",
                        "arguments": {
                            "a": {"type": "variable", "name": "n"},
                            "b": {"type": "literal", "value": 0},
                        },
                    },
                    "true_branch": {"type": "literal", "value": 0},
                    "false_branch": {
                        "type": "call",
                        "function": "countdown",
                        "arguments": [
                            {
                                "type": "tool_call",
                                "tool": "subtract",
                                "arguments": {
                                    "a": {"type": "variable", "name": "n"},
                                    "b": {"type": "literal", "value": 1},
                                },
                            }
                        ],
                    },
                },
            }
        ],
        "body": [
            {
                "type": "call",
                "function": "countdown",
                "arguments": [{"type": "literal", "value": 100}],
            }
        ],
        "mode": "single",
    }
)

result = await execute_v2(
    program,
    provider,
    limits=ExecutionLimits(
        max_call_depth=101,
        max_nodes=1_000,
        max_tool_calls=200,
        timeout_seconds=10,
    ),
)

max_call_depth is inclusive and counts active user-function calls. It is separate from max_depth, which constrains the declared expression structure. All limits and counters are shared across the program invocation.

The version 2 interpreter uses explicit frames rather than Python recursion, but recursive programs should always configure call-depth, node, tool-call, and wall-clock limits. Compilation into tools, traversal helpers, tree descriptions, and the stable root API do not support version 2 yet.