Compare commits

...
Author SHA1 Message Date
praisonai-triage-agent[bot]andMervin Praison 68064594dc fix: preserve cancelled status for empty durable turns
Check last_stop_reason == "cancelled" before the None-result branch so
an OpenAI-native cancellation that returns None is journaled as
cancelled rather than failed, in both sync and async durable paths.

Co-authored-by: Mervin Praison <[email protected]>
2026-08-13 16:36:53 +00:00
2 changed files with 59 additions and 18 deletions

No files matched your search

@@ -2703,15 +2703,12 @@ Your Goal: {self.goal}"""
)
result = self._chat_impl(prompt, temperature, tools, output_json, output_pydantic, reasoning_steps, stream, task_name, task_description, task_id, config, force_retrieval, skip_retrieval, attachments, _trace_emitter, tool_choice, seed=seed, cancel_token=_cancel)
if durable_context is not None:
outcome = (
"failed"
if result is None
else (
"cancelled"
if getattr(self, "last_stop_reason", None) == "cancelled"
else "succeeded"
)
)
if getattr(self, "last_stop_reason", None) == "cancelled":
outcome = "cancelled"
elif result is None:
outcome = "failed"
else:
outcome = "succeeded"
durable_context.finalize(outcome)
self.execution.resume_run_id = None
return result
@@ -3390,15 +3387,12 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
seed=seed, cancel_token=cancel_token
)
if durable_context is not None:
outcome = (
"failed"
if result is None
else (
"cancelled"
if getattr(self, "last_stop_reason", None) == "cancelled"
else "succeeded"
)
)
if getattr(self, "last_stop_reason", None) == "cancelled":
outcome = "cancelled"
elif result is None:
outcome = "failed"
else:
outcome = "succeeded"
await durable_context.afinalize(outcome)
self.execution.resume_run_id = None
return result
@@ -429,6 +429,53 @@ async def test_agent_achat_none_result_finalizes_run(tmp_path):
assert agent.execution.resume_run_id is None
def test_agent_chat_cancelled_none_result_finalizes_run(tmp_path):
from praisonaiagents import Agent
path = str(tmp_path / "journal.db")
agent = Agent(
name="durable-agent",
instructions="test",
execution=ExecutionConfig(durable=True, journal_path=path),
)
agent.llm_instance = SimpleNamespace(_last_stop_reason="cancelled")
with patch.object(agent, "_chat_impl", return_value=None):
assert agent.chat("task") is None
run_id = agent.last_durable_run_id
journal = RunJournal(path)
assert journal.run_meta(run_id).status == "cancelled"
assert journal.interrupted_runs() == []
journal.close()
assert agent.execution.resume_run_id is None
@pytest.mark.asyncio
async def test_agent_achat_cancelled_none_result_finalizes_run(tmp_path):
from praisonaiagents import Agent
path = str(tmp_path / "journal.db")
agent = Agent(
name="durable-agent",
instructions="test",
execution=ExecutionConfig(durable=True, journal_path=path),
)
agent.llm_instance = SimpleNamespace(_last_stop_reason="cancelled")
with patch.object(agent, "_achat_impl", AsyncMock(return_value=None)):
assert await agent.achat("task") is None
run_id = agent.last_durable_run_id
journal = RunJournal(path)
assert journal.run_meta(run_id).status == "cancelled"
assert journal.interrupted_runs() == []
journal.close()
assert agent.execution.resume_run_id is None
def test_custom_llm_fatal_tool_error_reaches_durable_lifecycle(tmp_path):
from praisonaiagents import Agent