Skip to content

feat(filter): add agentic_loop controller for Responses API #294

Description

@leseb

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

Status
In Progress

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions