Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/praisonai-agents/praisonaiagents/agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -3751,6 +3751,11 @@ def last_stop_reason(self) -> str:
# the agent (see ``_extract_llm_response_content``); prefer it so a
# provider block/refusal isn't masked by a backend default of "completed".
own = getattr(self, '_last_stop_reason', None)
# Prefer a specific agent-owned provider block/refusal/truncation before
# any backend fallback so a stale backend reason (e.g. ``max_steps``)
# cannot mask the OpenAI-native classification recorded for this turn.
if own and own != "completed":
return own
# Read from the already-instantiated backends only. ``__openai_client``
# is the raw (name-mangled) attribute, never the lazy ``_openai_client``
# property, so this never triggers OpenAI client creation for
Expand Down
7 changes: 7 additions & 0 deletions src/praisonai-agents/praisonaiagents/agent/chat_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5766,6 +5766,13 @@ def _model_fn(req):

async def _achat_completion_with_retry_core(self, messages, temperature=None, tools=None, stream=None, reasoning_steps=False, task_name=None, task_description=None, task_id=None, response_format=None, stream_callback=None, emit_events=True):
"""Async retry/backoff core for chat completion (middleware-agnostic)."""
# Reset the agent-level finish-reason classification at the start of each
# async OpenAI-native turn, mirroring the sync ``_chat_completion`` reset,
# so a provider block/refusal recorded on a previous run (see
# ``_extract_llm_response_content``) never leaks into this one. The LiteLLM
# path resets its own backend flag independently.
self._last_stop_reason = "completed"

retry_config = getattr(self, '_retry_config', None)
if not retry_config:
return await self._execute_unified_achat_completion(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,9 +312,14 @@ def _outcome_for_result(self, result):
"""
from .run_outcome import RunOutcome, PROVIDER_BLOCK_REASONS
output = str(result) if result is not None else None
# Read the recorded classification without letting a lookup problem mask a
# provider block as a successful ``completed``. Only an expected
# missing-state lookup (``AttributeError``) is treated as "no reason
# recorded"; anything else propagates rather than silently succeeding.
reason = None
try:
reason = getattr(self, "last_stop_reason", None)
except Exception:
except AttributeError:
reason = None
if reason in PROVIDER_BLOCK_REASONS:
return RunOutcome(reason=reason, output=output)
Expand Down
30 changes: 30 additions & 0 deletions src/praisonai-agents/tests/test_run_outcome.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,33 @@ def test_astart_outcome_surfaces_refusal():
)
assert o.reason == "refused"
assert o.succeeded is False


def test_agent_last_stop_reason_prefers_own_block_over_stale_backend():
# Regression: an agent-owned provider block (OpenAI-native classification)
# must win over a stale backend reason like "max_steps"; otherwise the
# actionable "refused" is masked and _outcome_for_result reports success.
from praisonaiagents.agent.agent import Agent

agent = Agent(instructions="test", llm="gpt-4o-mini")
agent._last_stop_reason = "refused"

class _StaleBackend:
_last_stop_reason = "max_steps"

agent.llm_instance = _StaleBackend()
assert agent.last_stop_reason == "refused"


def test_agent_last_stop_reason_backend_used_when_own_completed():
# When the agent recorded nothing specific, the backend reason still wins.
from praisonaiagents.agent.agent import Agent

agent = Agent(instructions="test", llm="gpt-4o-mini")
agent._last_stop_reason = "completed"

class _Backend:
_last_stop_reason = "max_steps"

agent.llm_instance = _Backend()
assert agent.last_stop_reason == "max_steps"
Loading