Summary
Add the agentic_loop filter — a pure loop controller for the Responses API pipeline. Manages the inference loop lifecycle: iteration counting, tool-choice reset, exit conditions, and the loop/done signal that branch chains read to decide whether to re-enter inference.
Does not classify tool calls by type or execute them — classification is handled by tool_parse; execution by sub-filters routed via branch chains.
What's implemented
- Loop control: writes
filter_results (agentic_loop.action = "loop" or "done") during on_request — the only phase where Praxis evaluates branch chain conditions
- Exit conditions: no tool calls → done;
finish_reason == "length" / status == "incomplete" → incomplete; request-level max_tool_calls reached → incomplete; config-level max_infer_iters reached → incomplete
- State management: iteration increment,
tool_choice reset to "auto" after first iteration, tool_calls cleared after dispatch
- Config:
max_infer_iters (default 10), deny_unknown_fields
- ResponsesState creation:
openai_responses_validate now creates ResponsesState for all Responses API create requests (not just rehydrate paths)
- 23 unit tests, example config, generated filter docs
Branch chain re-entrance
The loop is driven by praxis-core's ReEnter rejoin mode. A branch chain on agentic_loop matches action = loop and rejoins at the named responses_proxy filter:
- filter: responses_proxy
name: inference
- filter: agentic_loop
max_infer_iters: 10
branch_chains:
- name: tool-loop
on_result:
filter: agentic_loop
key: action
result: loop
rejoin: inference
max_iterations: 15
chains:
- name: tool-execution
filters:
- filter: headers
request_add:
- name: X-Agentic-Loop
value: "true"
Two iteration limits serve complementary purposes:
max_infer_iters (filter config) — application-level cap that marks the response as incomplete when reached
max_iterations (branch chain) — infrastructure-level safety cap that prevents infinite loops
Key design decision
Loop control runs in on_request, not on_response. Branch chains in Praxis only evaluate on_result conditions during the request phase (after on_request). On the first pass, tool_calls is empty so the filter signals done. After stream_events/tool_parse populate tool_calls during response-body processing, the branch chain re-enters the pipeline and on_request runs again with populated tool calls.
What's deferred
- Tool call classification (
tool_parse — future filter)
- Tool execution sub-filters (MCP, web search, file search — routed via branch chains)
- Terminal-event reconciliation (stream_events concern, not agentic_loop)
Files
apis/src/openai/responses/agentic_loop/ — mod.rs, config.rs, tests.rs (new)
apis/src/openai/responses/validate/mod.rs — ResponsesState creation
apis/src/openai/responses/state.rs — updated doc comments
apis/src/openai/mod.rs, apis/src/openai/responses/mod.rs — exports
server/src/lib.rs — filter registration
examples/configs/openai/responses/agentic-loop.yaml — example config (new)
examples/configs/openai/responses/full-flow.yaml — added agentic_loop with branch chains
docs/filters/agentic_loop.md — generated filter doc (new)
xtask/src/lint_example_tests.rs — skip list
Closes #26
Summary
Add the
agentic_loopfilter — a pure loop controller for the Responses API pipeline. Manages the inference loop lifecycle: iteration counting, tool-choice reset, exit conditions, and the loop/done signal that branch chains read to decide whether to re-enter inference.Does not classify tool calls by type or execute them — classification is handled by
tool_parse; execution by sub-filters routed via branch chains.What's implemented
filter_results(agentic_loop.action="loop"or"done") duringon_request— the only phase where Praxis evaluates branch chain conditionsfinish_reason == "length"/status == "incomplete"→ incomplete; request-levelmax_tool_callsreached → incomplete; config-levelmax_infer_itersreached → incompletetool_choicereset to"auto"after first iteration,tool_callscleared after dispatchmax_infer_iters(default 10),deny_unknown_fieldsopenai_responses_validatenow createsResponsesStatefor all Responses API create requests (not just rehydrate paths)Branch chain re-entrance
The loop is driven by praxis-core's
ReEnterrejoin mode. A branch chain onagentic_loopmatchesaction = loopand rejoins at the namedresponses_proxyfilter:Two iteration limits serve complementary purposes:
max_infer_iters(filter config) — application-level cap that marks the response asincompletewhen reachedmax_iterations(branch chain) — infrastructure-level safety cap that prevents infinite loopsKey design decision
Loop control runs in
on_request, noton_response. Branch chains in Praxis only evaluateon_resultconditions during the request phase (afteron_request). On the first pass,tool_callsis empty so the filter signals done. Afterstream_events/tool_parsepopulatetool_callsduring response-body processing, the branch chain re-enters the pipeline andon_requestruns again with populated tool calls.What's deferred
tool_parse— future filter)Files
apis/src/openai/responses/agentic_loop/— mod.rs, config.rs, tests.rs (new)apis/src/openai/responses/validate/mod.rs— ResponsesState creationapis/src/openai/responses/state.rs— updated doc commentsapis/src/openai/mod.rs,apis/src/openai/responses/mod.rs— exportsserver/src/lib.rs— filter registrationexamples/configs/openai/responses/agentic-loop.yaml— example config (new)examples/configs/openai/responses/full-flow.yaml— added agentic_loop with branch chainsdocs/filters/agentic_loop.md— generated filter doc (new)xtask/src/lint_example_tests.rs— skip listCloses #26