mirror of
https://github.com/usestrix/strix.git
synced 2026-10-28 06:23:07 +00:00
fix: salvage a crashed run's full history into the session so revival loses no context
This commit is contained in:
parent
744877f78a
commit
2c2cc193e3
2 changed files with 69 additions and 1 deletions
|
|
@ -30,6 +30,7 @@ from strix.core.inputs import child_initial_input
|
|||
from strix.core.sessions import (
|
||||
enforce_image_budget,
|
||||
open_agent_session,
|
||||
replace_session_items,
|
||||
seed_initial_input,
|
||||
strip_all_images_from_session,
|
||||
)
|
||||
|
|
@ -117,6 +118,29 @@ def _transient_model_retry_delay(attempt: int) -> float:
|
|||
return min(delay, _TRANSIENT_MODEL_RETRY_MAX_DELAY_S)
|
||||
|
||||
|
||||
async def _salvage_stream_to_session(
|
||||
session: Session,
|
||||
pre_run_items: list[Any],
|
||||
stream: Any,
|
||||
agent_id: str,
|
||||
) -> None:
|
||||
"""Persist a crashed run's full history so a revived agent loses no context."""
|
||||
if stream is None:
|
||||
return
|
||||
try:
|
||||
replay = list(stream.to_input_list())
|
||||
except Exception:
|
||||
logger.exception("could not build salvage history for %s", agent_id)
|
||||
return
|
||||
desired = list(pre_run_items) + replay
|
||||
if len(desired) <= len(pre_run_items):
|
||||
return
|
||||
try:
|
||||
await replace_session_items(session, desired)
|
||||
except Exception:
|
||||
logger.exception("salvaging crashed run history failed for %s", agent_id)
|
||||
|
||||
|
||||
async def _seed_and_prepare_first_input(
|
||||
session: Session | None, initial_input: Any, *, start_parked: bool
|
||||
) -> Any:
|
||||
|
|
@ -534,6 +558,8 @@ async def _run_cycle( # noqa: PLR0912, PLR0915
|
|||
compactions = 0
|
||||
model_retries = 0
|
||||
while True:
|
||||
stream: Any = None
|
||||
pre_run_items: list[Any] = []
|
||||
try:
|
||||
await coordinator.mark_running(agent_id)
|
||||
if session is not None:
|
||||
|
|
@ -547,6 +573,8 @@ async def _run_cycle( # noqa: PLR0912, PLR0915
|
|||
await _compact_session(agent, session, run_config, force=False)
|
||||
except Exception:
|
||||
logger.exception("proactive compaction failed for %s", agent_id)
|
||||
with contextlib.suppress(Exception):
|
||||
pre_run_items = list(await session.get_items())
|
||||
stream = Runner.run_streamed(
|
||||
agent,
|
||||
input=input_data,
|
||||
|
|
@ -657,6 +685,8 @@ async def _run_cycle( # noqa: PLR0912, PLR0915
|
|||
if session is not None:
|
||||
input_data = []
|
||||
continue
|
||||
if session is not None:
|
||||
await _salvage_stream_to_session(session, pre_run_items, stream, agent_id)
|
||||
if not interactive:
|
||||
raise
|
||||
if isinstance(exc, MaxTurnsExceeded):
|
||||
|
|
@ -671,7 +701,7 @@ async def _run_cycle( # noqa: PLR0912, PLR0915
|
|||
return None
|
||||
else:
|
||||
await _settle_run_result(coordinator, agent_id, interactive)
|
||||
return stream
|
||||
return cast("RunResultBase | None", stream)
|
||||
|
||||
|
||||
async def _settle_run_result(
|
||||
|
|
|
|||
|
|
@ -595,6 +595,44 @@ async def test_run_cycle_parked_parks_instead_of_raising(
|
|||
assert coordinator.errors["root"] == "unexpected explosion"
|
||||
|
||||
|
||||
class _SalvageStream:
|
||||
def __init__(self, replay: list[dict[str, Any]]) -> None:
|
||||
self._replay = replay
|
||||
|
||||
def to_input_list(self) -> list[dict[str, Any]]:
|
||||
return self._replay
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_salvage_stream_to_session_preserves_full_history(tmp_path: Any) -> None:
|
||||
session = SQLiteSession("child", tmp_path / "agents.db")
|
||||
await session.add_items([{"role": "user", "content": "identity + task"}])
|
||||
pre_run = list(await session.get_items())
|
||||
|
||||
# A crash mid-run: the stream produced two turns the SDK never committed.
|
||||
stream = _SalvageStream(
|
||||
[
|
||||
{"role": "assistant", "content": "recon turn 1"},
|
||||
{"role": "assistant", "content": "recon turn 2"},
|
||||
]
|
||||
)
|
||||
await execution._salvage_stream_to_session(session, pre_run, stream, "child")
|
||||
|
||||
stored = [cast("dict[str, Any]", i) for i in await session.get_items()]
|
||||
assert [i["content"] for i in stored] == [
|
||||
"identity + task",
|
||||
"recon turn 1",
|
||||
"recon turn 2",
|
||||
]
|
||||
|
||||
# A crash with nothing new to salvage leaves the session untouched.
|
||||
await execution._salvage_stream_to_session(
|
||||
session, list(await session.get_items()), _SalvageStream([]), "child"
|
||||
)
|
||||
assert len(await session.get_items()) == 3
|
||||
session.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_seed_initial_input_persists_and_is_idempotent(tmp_path: Any) -> None:
|
||||
session = SQLiteSession("child", tmp_path / "agents.db")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue