fix: persist agent identity/task before first run cycle so it survives a first-turn crash

This commit is contained in:
Ahmed Allam
2026-07-28 12:37:53 +00:00
parent d83bb7360a
commit 744877f78a
3 changed files with 88 additions and 2 deletions
+18 -2
View File
@@ -30,6 +30,7 @@ from strix.core.inputs import child_initial_input
from strix.core.sessions import (
enforce_image_budget,
open_agent_session,
seed_initial_input,
strip_all_images_from_session,
)
from strix.llm.compaction import is_context_overflow, maybe_compact
@@ -116,6 +117,17 @@ def _transient_model_retry_delay(attempt: int) -> float:
return min(delay, _TRANSIENT_MODEL_RETRY_MAX_DELAY_S)
async def _seed_and_prepare_first_input(
session: Session | None, initial_input: Any, *, start_parked: bool
) -> Any:
"""Persist the opening input up front so it survives a first-turn crash."""
if initial_input and session is not None and not start_parked:
with contextlib.suppress(Exception):
if await seed_initial_input(session, initial_input):
return []
return initial_input
async def run_agent_loop(
*,
agent: Any,
@@ -138,6 +150,10 @@ async def run_agent_loop(
)
result: RunResultBase | None = None
first_cycle_input = await _seed_and_prepare_first_input(
session, initial_input, start_parked=start_parked
)
budget_stopped = coordinator.budget_stopped
reserve_stopped = coordinator.reserve_stopped
if budget_stopped:
@@ -157,7 +173,7 @@ async def run_agent_loop(
agent,
coordinator,
agent_id,
input_data=initial_input,
input_data=first_cycle_input,
run_config=run_config,
context=context,
max_turns=max_turns,
@@ -170,7 +186,7 @@ async def run_agent_loop(
agent,
coordinator,
agent_id,
initial_input=initial_input,
initial_input=first_cycle_input,
run_config=run_config,
context=context,
max_turns=max_turns,
+13
View File
@@ -7,6 +7,7 @@ import logging
from typing import TYPE_CHECKING, Any, cast
from weakref import WeakKeyDictionary
from agents.items import ItemHelpers
from agents.memory import SQLiteSession
@@ -26,6 +27,18 @@ def open_agent_session(agent_id: str, path: Path) -> SQLiteSession:
return SQLiteSession(session_id=agent_id, db_path=path)
async def seed_initial_input(session: Session, initial_input: Any) -> bool:
"""Commit an agent's opening identity/task input before its first run cycle."""
items = ItemHelpers.input_to_new_input_list(initial_input)
if not items:
return False
async with session_write_lock(session):
if await session.get_items():
return False
await session.add_items(items)
return True
_IMAGE_REJECTED_TEXT = "[image rejected by the model]"
_IMAGE_ELIDED_TEXT = "[older screenshot elided to bound context memory]"
_INHERITED_IMAGE_TEXT = "[screenshot omitted from inherited context]"
+57
View File
@@ -17,6 +17,7 @@ from strix.core.execution import (
_notify_parent_on_terminal,
_notify_root_on_budget_reserve,
)
from strix.core.sessions import seed_initial_input
from strix.tools.finish.tool import finish_scan
@@ -592,3 +593,59 @@ async def test_run_cycle_parked_parks_instead_of_raising(
assert result is None
assert coordinator.statuses["root"] == "failed"
assert coordinator.errors["root"] == "unexpected explosion"
@pytest.mark.asyncio
async def test_seed_initial_input_persists_and_is_idempotent(tmp_path: Any) -> None:
session = SQLiteSession("child", tmp_path / "agents.db")
identity = [{"role": "user", "content": "You are agent recon (abc); do X."}]
assert await seed_initial_input(session, identity) is True
assert len(await session.get_items()) == 1
# A populated session is left untouched (no duplicate identity message).
assert await seed_initial_input(session, identity) is False
assert len(await session.get_items()) == 1
assert await seed_initial_input(session, []) is False
session.close()
@pytest.mark.asyncio
async def test_run_agent_loop_seeds_identity_before_first_cycle(
tmp_path: Any, monkeypatch: pytest.MonkeyPatch
) -> None:
coordinator = AgentCoordinator()
await coordinator.register("child", "recon", parent_id="root")
session = SQLiteSession("child", tmp_path / "agents.db")
captured: dict[str, Any] = {}
async def _crash_first_turn(*_args: Any, **kwargs: Any) -> Any:
captured["input_data"] = kwargs.get("input_data")
captured["items_at_start"] = await session.get_items()
raise RuntimeError("first-turn crash")
monkeypatch.setattr(execution, "_run_cycle", _crash_first_turn)
identity = [{"role": "user", "content": "You are agent recon (abc); maintain your identity."}]
with pytest.raises(RuntimeError, match="first-turn crash"):
await execution.run_agent_loop(
agent=object(),
initial_input=identity,
run_config=None, # type: ignore[arg-type]
context={"agent_id": "child", "parent_id": "root"},
max_turns=5,
coordinator=coordinator,
agent_id="child",
interactive=False,
session=session,
)
# The first cycle ran with an empty input against the pre-seeded session.
assert captured["input_data"] == []
assert captured["items_at_start"]
# The identity/task survives the first-turn crash, so a revival can resume it.
stored = await session.get_items()
assert any("recon" in str(cast("dict[str, Any]", i).get("content", "")) for i in stored)
session.close()