OpenProgram Docs

Agentic self-recursion guard

Current behavior: agentic functions receive situational guidance in the user turn, while a per-function recursion-depth ceiling remains as a deterministic backstop.
Source of truth: openprogram/agentic_programming/call_state.py and openprogram/agentic_programming/runtime.py.
Tests: tests/unit/programs/runtime/test_self_recursion_guard.py.

1. Problem

An agentic function runs an inner agent loop. Its tool list may contain the same function, and a matching docstring can cause the inner model to call the function again. Repeated direct calls can otherwise create unbounded same-name recursion.

2. Situational guidance

openprogram/agentic_programming/runtime.py::_situational_prefix creates an English user-turn prefix that states the current function name, explains that calling it would re-enter the current execution, and directs the model to lower-level tools.

  • The prefix is injected at the beginning of the user content block on the regular execution path.
  • The standalone fallback derives the deepest function name from the recursion context and applies the same prefix.
  • The system prompt remains separate. The current function is not hidden through a self-deny entry; the guidance has a visible subject and the model decides not to call it.

3. Depth ceiling

  • openprogram/agentic_programming/call_state.py::_MAX_AGENTIC_RECURSION_DEPTH is 5.
  • _recursion_depth is a context-local map keyed by function name.
  • The synchronous and asynchronous wrappers increment the current function's depth, raise RecursionError when the limit would be exceeded, and reset the context value in finally.
  • The ceiling counts each function name independently. It protects direct A → A recursion; cross-function cycles such as A → B → A are a documented remaining limitation.

4. Source symbols

MechanismCurrent source symbol
Depth ceilingopenprogram/agentic_programming/call_state.py::_MAX_AGENTIC_RECURSION_DEPTH
Depth contextopenprogram/agentic_programming/call_state.py::_recursion_depth
Sync and async wrappersopenprogram/agentic_programming/call_state.py
Situational prefixopenprogram/agentic_programming/runtime.py::_situational_prefix
Regular injectionopenprogram/agentic_programming/runtime.py frame-prefix construction and user-content assembly
Standalone fallbackopenprogram/agentic_programming/runtime.py recursion-context fallback
Tool policyopenprogram/agentic_programming/runtime.py retains source/allow/toolset and unrelated deny handling

5. Behavior contract

ContractEvidence
Guidance names the current function, says not to call it, and explains recursion.test_situational_prefix_warns_against_self_call
Empty docstrings do not add an irrelevant function-job suffix.test_situational_prefix_handles_empty_doc
The current function name is not added to self-deny during the call.test_self_name_NOT_denied_during_call
Depth increments during a call and resets after return or exception.test_depth_increments_during_call, test_depth_restored_after_return, test_depth_restored_after_exception
Direct same-name recursion raises at the configured ceiling; distinct subcalls do not cause collateral rejection.test_depth_backstop_raises_past_limit, test_distinct_subcalls_not_collateral_damage

6. Boundaries and remaining work

  • The guidance is not a guarantee that a weak model will always stop before the ceiling.
  • Cross-function cycle detection is not implemented by this mechanism.
  • The guard does not change tool authorization, session DAG semantics, or the unified system-prefix contract.
  • This page intentionally uses symbols and repository paths instead of fixed line numbers or historical commit identifiers; source movement should not silently make the evidence stale.
Last updated · 2026-10-10