Skip to content

compat: treat natural no_tool finish as a complete run - #24

Open
dappweb wants to merge 1 commit into
ApodexAI:mainfrom
dappweb:compat/no-tool-final-answer
Open

compat: treat natural no_tool finish as a complete run#24
dappweb wants to merge 1 commit into
ApodexAI:mainfrom
dappweb:compat/no-tool-final-answer

Conversation

@dappweb

@dappweb dappweb commented Aug 28, 2026

Copy link
Copy Markdown

Summary

Two parity gaps break the stateful react workflow for OpenAI-compatible models that end a task with a plain-text answer instead of a final tool call. Reproduced with a local Qwen3.5-family endpoint (qwen3.8-27b via Ollama /v1).

Changes

  1. workflows/stateful_react_agent/_runtime.py — add no_tool to _GRACEFUL_FINAL_STOP_REASONS so force_final_answer() rescue runs when the loop ends naturally after the model has already produced its answer.
  2. apodex/task_runner.py — pass no_tool_is_complete=True in the native-workflow completion check, matching the generic-loop path (which already passes it).

Verification

  • Before: local qwen3.8-27b-fast completes the task (reads input, writes deliverable content) but the run is reported as run incomplete · stopped_by=no_tool · partial output was not saved as a final report.
  • After: same run completes and the final report is rendered/saved.

Notes

  • Generic loop already passes no_tool_is_complete=True; this makes the native workflow consistent.
  • No behavior change for workflows that finish via explicit final-answer tools.
Two parity gaps for OpenAI-compatible models that end a task with a
plain-text answer instead of a final tool call (observed with
Qwen3.5-based local endpoints):

1. workflows/stateful_react_agent/_runtime.py: _GRACEFUL_FINAL_STOP_
   REASONS does not include "no_tool", so force_final_answer() rescue
   is skipped even though the model produced its answer and the loop
   ended naturally.
2. apodex/task_runner.py: the native-workflow path calls _is_complete_
   run() without no_tool_is_complete=True, unlike the generic loop
   path (L396), so the same natural finish is reported as
   "run incomplete" and the report is not saved.

With both applied, a local qwen3.8-27b endpoint completes the
stateful react workflow end to end and the final report is saved.
@zhanghanduo

Copy link
Copy Markdown
Collaborator

Thanks for the attempt to fix.

I think the reported issue is valid, but the root cause is that no_tool is overloaded: depending on the workflow, it can mean a successful plain-text answer, an empty response, a tool-protocol violation, or exhausted nudge retries.

In stateful_react, no_tool_behavior="stop", so a non-empty plain-text response is already a successful final answer. In agent_team, non-empty text is finalized by BareTextFinalizeObserver, while no_tool usually means empty-response retries were exhausted.

Therefore, globally treating no_tool as complete is unsafe, and routing it through force_final_answer() adds an unnecessary LLM call that can rewrite the answer and mark it as best_effort.

I suggest introducing explicit stop reasons such as plain_text_final and empty_response_retries_exhausted, or having each workflow return an authoritative completion status. That would address the root cause instead of making TaskRunner infer completion from the ambiguous no_tool value.

@zhanghanduo zhanghanduo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reported stateful_react issue is valid, but this fix is too broad.

Passing no_tool_is_complete=True from _run_native_workflow() makes every
native-workflow stopped_by="no_tool" a completed delivery, even though
no_tool is not a uniform success signal across workflows.

In agent_team, successful plain-text completion is converted to
final_answer by BareTextFinalizeObserver. The remaining no_tool cases
typically represent exhausted empty-response/nudge retries and are incomplete.
Marking them complete would regress the behavior covered by:

apodex/tests/test_features.py::test_native_workflow_no_tool_stop_is_not_reported_as_delivery

Please distinguish successful plain-text completion from exhausted no-tool
retries—through explicit stop reasons or an authoritative workflow completion
status—rather than treating no_tool as globally successful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants