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:
Tests:
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_DEPTHis 5._recursion_depthis a context-local map keyed by function name.- The synchronous and asynchronous wrappers increment the current function's depth, raise
RecursionErrorwhen the limit would be exceeded, and reset the context value infinally. - 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
| Mechanism | Current source symbol |
|---|---|
| Depth ceiling | openprogram/agentic_programming/call_state.py::_MAX_AGENTIC_RECURSION_DEPTH |
| Depth context | openprogram/agentic_programming/call_state.py::_recursion_depth |
| Sync and async wrappers | openprogram/agentic_programming/call_state.py |
| Situational prefix | openprogram/agentic_programming/runtime.py::_situational_prefix |
| Regular injection | openprogram/agentic_programming/runtime.py frame-prefix construction and user-content assembly |
| Standalone fallback | openprogram/agentic_programming/runtime.py recursion-context fallback |
| Tool policy | openprogram/agentic_programming/runtime.py retains source/allow/toolset and unrelated deny handling |
5. Behavior contract
| Contract | Evidence |
|---|---|
| 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