diff --git a/strix/core/execution.py b/strix/core/execution.py index d26486de0..91fd76b75 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,16 @@ 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) + 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 153d16ddb..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 @@ -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..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,6 +575,9 @@ async def agent_finish( ) await coordinator.set_status(me, "completed") + if report_state := get_global_report_state(): + report_state.save_run_data() + return json.dumps( { "success": True,