From 744d28c1eeb9dc038b3c4a5b6b433cc82fe92432 Mon Sep 17 00:00:00 2001 From: Tanmay9223 Date: Sun, 19 Jul 2026 18:31:14 +0530 Subject: [PATCH 1/2] fix(core): handle MaxTurnsExceeded and flush SARIF (fixes #797) When child agents get stuck in Non-Lifecycle Final Output (NFE) loops in non-interactive scans, they raise a MaxTurnsExceeded exception. Previously, this exception went unhandled in the child loop task, crashing the root scan process without saving the SARIF report for agents that had already completed their work. This change gracefully handles MaxTurnsExceeded and flushes: - strix/core/execution.py: Catch MaxTurnsExceeded in the child loop. - strix/core/runner.py: Gracefully stop scan on root MaxTurnsExceeded. - strix/tools/agents_graph/tools.py: Save run data in agent_finish. The SARIF report is now saved incrementally, ensuring partial results are preserved even if a subset of agents fail to complete properly. --- strix/core/execution.py | 6 +++++- strix/core/runner.py | 12 ++++++++++++ strix/tools/agents_graph/tools.py | 4 ++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/strix/core/execution.py b/strix/core/execution.py index d26486de0..45082c2f4 100644 --- a/strix/core/execution.py +++ b/strix/core/execution.py @@ -284,7 +284,7 @@ async def _run_noninteractive_until_lifecycle( result: RunResultBase | None = None input_data: Any = initial_input invalid_final_outputs = 0 - invalid_final_output_limit = max(1, max_turns) + invalid_final_output_limit = min(5, max(1, max_turns)) while True: if coordinator.budget_stopped: @@ -581,6 +581,10 @@ async def _child_loop() -> None: ) except BudgetExceededError: logger.info("child %s stopped after reaching the scan budget limit", child_id) + except MaxTurnsExceeded as exc: + logger.warning("child %s crashed due to MaxTurnsExceeded: %s", child_id, exc) + except Exception: + logger.exception("child %s crashed with unhandled exception", child_id) task_handle = asyncio.create_task(_child_loop(), name=f"agent-{name}-{child_id}") await coordinator.attach_runtime(child_id, task=task_handle) diff --git a/strix/core/runner.py b/strix/core/runner.py index 153d16ddb..e0e885548 100644 --- a/strix/core/runner.py +++ b/strix/core/runner.py @@ -30,6 +30,7 @@ spawn_child_agent as start_child_agent, ) from strix.core.hooks import BudgetExceededError, ReportUsageHooks +from agents.exceptions import MaxTurnsExceeded from strix.core.inputs import ( DEFAULT_MAX_TURNS, build_root_task, @@ -391,6 +392,17 @@ async def spawn_child_agent(**kwargs: Any) -> dict[str, Any]: with contextlib.suppress(Exception): await coordinator.set_status(root_id, "stopped") return None + except MaxTurnsExceeded as exc: + logger.warning( + "Scan %s stopped: MaxTurnsExceeded (%s).", + scan_id, + exc, + ) + if root_id is not None: + await coordinator.cancel_descendants(root_id) + with contextlib.suppress(Exception): + await coordinator.set_status(root_id, "failed") + return None except BaseException: logger.exception("Strix scan %s failed", scan_id) if root_id is not None: diff --git a/strix/tools/agents_graph/tools.py b/strix/tools/agents_graph/tools.py index 478c1efb7..6c720718a 100644 --- a/strix/tools/agents_graph/tools.py +++ b/strix/tools/agents_graph/tools.py @@ -574,6 +574,10 @@ async def agent_finish( ) await coordinator.set_status(me, "completed") + from strix.report.state import get_global_report_state + if report_state := get_global_report_state(): + report_state.save_run_data() + return json.dumps( { "success": True, From a01a9e052f32e5a5dc2ad82d3cf14bda458f00de Mon Sep 17 00:00:00 2001 From: Tanmay9223 Date: Mon, 20 Jul 2026 01:57:28 +0530 Subject: [PATCH 2/2] fix(core): address PR review comments --- strix/core/execution.py | 6 ++++++ strix/core/runner.py | 2 +- strix/tools/agents_graph/tools.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/strix/core/execution.py b/strix/core/execution.py index 45082c2f4..91fd76b75 100644 --- a/strix/core/execution.py +++ b/strix/core/execution.py @@ -583,8 +583,14 @@ async def _child_loop() -> None: logger.info("child %s stopped after reaching the scan budget limit", child_id) except MaxTurnsExceeded as exc: logger.warning("child %s crashed due to MaxTurnsExceeded: %s", child_id, exc) + with contextlib.suppress(Exception): + await coordinator.set_status(child_id, "crashed") + await _notify_parent_on_crash(coordinator, child_id, "crashed") except Exception: logger.exception("child %s crashed with unhandled exception", child_id) + with contextlib.suppress(Exception): + await coordinator.set_status(child_id, "crashed") + await _notify_parent_on_crash(coordinator, child_id, "crashed") task_handle = asyncio.create_task(_child_loop(), name=f"agent-{name}-{child_id}") await coordinator.attach_runtime(child_id, task=task_handle) diff --git a/strix/core/runner.py b/strix/core/runner.py index e0e885548..4aa0cd7cb 100644 --- a/strix/core/runner.py +++ b/strix/core/runner.py @@ -11,6 +11,7 @@ from agents import RunConfig from agents.sandbox import SandboxRunConfig +from agents.exceptions import MaxTurnsExceeded from openai import RateLimitError from strix.agents.factory import build_strix_agent, make_child_factory @@ -30,7 +31,6 @@ spawn_child_agent as start_child_agent, ) from strix.core.hooks import BudgetExceededError, ReportUsageHooks -from agents.exceptions import MaxTurnsExceeded from strix.core.inputs import ( DEFAULT_MAX_TURNS, build_root_task, diff --git a/strix/tools/agents_graph/tools.py b/strix/tools/agents_graph/tools.py index 6c720718a..366b3ebb1 100644 --- a/strix/tools/agents_graph/tools.py +++ b/strix/tools/agents_graph/tools.py @@ -13,6 +13,7 @@ from agents import RunContextWrapper, function_tool from strix.core.agents import Status, coordinator_from_context +from strix.report.state import get_global_report_state from strix.skills import validate_requested_skills @@ -574,7 +575,6 @@ async def agent_finish( ) await coordinator.set_status(me, "completed") - from strix.report.state import get_global_report_state if report_state := get_global_report_state(): report_state.save_run_data()