feat(responses): unify tool dispatch under a single agentic-loop owner (#1046) - #1133
Conversation
praxis-proxy#1046) Make openai_agentic_loop the single owner and sole parser for the Responses agentic loop, demoting web_search, mcp_dispatch, and file_search to execution-only dispatchers with no response phase. The owner normalizes, accumulates, and classifies every output item into a shared accumulated_output, decides the single loop signal, and runs the one fallible finalizer (terminal- and buffered-only), leaving the streaming terminal frame to openai_stream_events. Add a dedicated mcp_classify module as the single source of truth for MCP disposition and approval policy; mcp_dispatch/approval.rs keeps only the durable approval round-trip (praxis-proxy#1029). File-search calls are carried by index (FileSearchAssignment) into accumulated_output rather than a second payload copy, and dispatchers mutate their assigned slots in place. DispatchFailure is converted by the owner into a buffered JSON rejection or a streaming SSE error frame. Per the OpenAI Responses contract, exempt MCP tool calls from max_tool_calls (built-in tools only) while retaining MCP's independent per-round and concurrency caps. Fail closed with 502 on mixed server/client function-call ownership before any external side effect. Refs praxis-proxy#1046 Signed-off-by: Sébastien Han <seb@redhat.com>
Migrate web-search-chat-completions.yaml on_result to key on openai_agentic_loop instead of the now execution-only openai_web_search dispatcher, so the search-then-model continuation loops under the single owner (issue praxis-proxy#1046). Regenerate apis/src/openai/responses/README.md to drop the response-phase hooks from the demoted web_search and mcp_dispatch dispatchers. Refs praxis-proxy#1046 Signed-off-by: Sébastien Han <seb@redhat.com>
franciscojavierarceo
left a comment
There was a problem hiding this comment.
streamed local completions can return an empty 200 SSE body.
on every streaming IRR re-entry, openai_stream_events::arm clears ResponsesState.response_object at apis/src/openai/responses/stream_events/mod.rs:164-174. when a max_tool_calls rejection or approval-only round then completes before another upstream response, finish_deferred_local_response calls encode_local_completion at apis/src/openai/responses/agentic_loop/mod.rs:407-426. encode_local_completion returns None whenever that response object is no longer an object at apis/src/openai/responses/stream_events/mod.rs:1333-1343.
the client receives a 200 text/event-stream response without response.completed (and, when applicable, [DONE]), so a valid streamed approval or tool-limit response cannot complete reliably. we need to preserve a terminal response template or build one from the stable response id and accumulated output, and add a regression that exercises approval-only and tool-limit termination after a streaming IRR re-entry.
Signed-off-by: Sébastien Han <seb@redhat.com>
Retain the previous streamed response template across IRR request-side re-entry so approval-only and tool-limit exits can emit a canonical response.completed event before another upstream response exists. Drop the fallback when upstream response headers arrive and include it in continuation-state accounting. Signed-off-by: Sébastien Han <seb@redhat.com>
Fixed in 6395539. Thanks! |
franciscojavierarceo
left a comment
There was a problem hiding this comment.
reviewed the current head, the preserved local streaming completions, agentic-loop ownership, targeted tests, and existing comments. no blocking issues found.
Keep branch current with origin/main (praxis-proxy#1153, praxis-proxy#1133). Signed-off-by: Sébastien Han <seb@redhat.com>
Summary
Makes
openai_agentic_loopthe single owner and sole parser of the Responses agentic loop, demotingopenai_web_search,openai_mcp_dispatch, andopenai_file_search_calloutto execution-only dispatchers with no response phase, so one filter normalizes/accumulates/classifies every output item, publishes the single loop signal, and runs the one fallible terminal-buffered finalizer. File-search calls are carried by index (FileSearchAssignment) into a sharedaccumulated_outputrather than copied,DispatchFailureis converted by the owner into a buffered JSON rejection or streaming SSE error frame, and a newmcp_classifymodule is the single source of truth for MCP disposition/approval whileapproval.rskeeps only the durable round-trip. MCP tool calls are exempted frommax_tool_calls(built-ins only) while retaining their own per-round/concurrency caps, and mixed server/client function-call ownership fails closed with 502 before any external side effect.Related issue
Refs #1046
Validation
cargo test -p praxis-ai-apis(3362 passed, 0 failed)openai_agentic_loop.rs,file_search_callout.rs,test_openai_responses_vllm.pymake lint— clippy-D warnings+ nightlyfmt --checkclean;make buildcleanChecklist
Signed-off-bytrailer.Breaking changes
Example config for the agentic loop is restructured (
agentic-loop.yamland the file-search example configs) to reflect the single-owner pipeline ordering; deployments that copied the prior per-tool response-phase wiring must adopt the owner-driven layout.