From 390cbde78326a33451eb5d7cfcf885ecf6d44db8 Mon Sep 17 00:00:00 2001 From: "Eric A. Litman" Date: Tue, 4 Aug 2026 03:05:26 -0400 Subject: [PATCH] fix: isolate adversarial stage sub-agents from spine persistence [refs OSWE-239] Co-Authored-By: Claude Fable 5 --- agent/reviewer_adversarial.py | 2 + tests/reviewer/test_reviewer_adversarial.py | 72 +++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/agent/reviewer_adversarial.py b/agent/reviewer_adversarial.py index 839014da1..84f335d1d 100644 --- a/agent/reviewer_adversarial.py +++ b/agent/reviewer_adversarial.py @@ -352,6 +352,8 @@ def _bounded_agent( system_prompt=prompt, tools=tools or [], backend=backend, + # Stage sub-agents must not join the spine's persistence. + checkpointer=False, response_format=ToolStrategy(response_format), middleware=cast(list[AgentMiddleware[Any, Any, Any]], _stage_middleware(middleware)), ) diff --git a/tests/reviewer/test_reviewer_adversarial.py b/tests/reviewer/test_reviewer_adversarial.py index 3946ad55c..997b998d6 100644 --- a/tests/reviewer/test_reviewer_adversarial.py +++ b/tests/reviewer/test_reviewer_adversarial.py @@ -14,6 +14,7 @@ from langchain_core.language_models.chat_models import BaseChatModel from langchain_core.messages import AIMessage from langchain_core.outputs import ChatGeneration, ChatResult +from langgraph.constants import CONFIG_KEY_CHECKPOINTER, Send from langgraph.graph.state import RunnableConfig from langgraph.runtime import Runtime from pydantic import Field @@ -1553,6 +1554,77 @@ async def test_bounded_finder_agent_calls_its_model_and_returns_structured_outpu assert "FinderOutput" in model.bound_tool_names, str(spec["name"]) +@pytest.mark.asyncio +async def test_stage_subagent_ignores_spine_checkpointer_and_thread( + caplog: pytest.LogCaptureFixture, +) -> None: + from deepagents.backends import StateBackend + from langgraph.checkpoint.memory import InMemorySaver + from langgraph.graph import END, START, StateGraph + + from agent.review.adversarial import AdversarialState, FinderOutput + + caplog.set_level(logging.WARNING) + specs = _finder_specs(cast(BaseChatModel, _StubToolCallModel(response_tool="FinderOutput"))) + spec = next(spec for spec in specs if spec["name"] == "correctness") + model = cast(_StubToolCallModel, spec["model"]) + finder_graph = _bounded_agent( + model=cast(BaseChatModel, model), + response_format=FinderOutput, + backend=StateBackend(), + tools=cast(list[Any], spec.get("tools", [])), + middleware=[ + *cast(list[Any], spec.get("middleware", [])), + ExcludeToolsMiddleware(excluded=frozenset({"task"})), + ], + ) + saver = InMemorySaver() + config = cast( + RunnableConfig, + { + "configurable": { + "thread_id": "adversarial-spine-replica", + "checkpoint_ns": "", + CONFIG_KEY_CHECKPOINTER: saver, + } + }, + ) + observed: dict[str, Any] = {} + + async def prepare(state: AdversarialState) -> dict[str, Any]: + del state + return {"finders_expected": ["correctness", "security"]} + + def fanout(state: dict[str, Any]) -> list[Send]: + return [Send("find", {"finder_name": name}) for name in state["finders_expected"]] + + async def find(state: AdversarialState) -> dict[str, Any]: + data = cast(dict[str, Any], state) + name = data["finder_name"] + try: + observed[name] = await _run_stage(finder_graph, "review the diff", config) + except Exception as exc: + observed[name] = exc + return {"finder_results": [{"finder": name, "candidates": [], "error": None}]} + + builder = StateGraph(AdversarialState) + builder.add_node("prepare", prepare) + builder.add_node("find", find) + builder.add_edge(START, "prepare") + builder.add_conditional_edges("prepare", fanout) + builder.add_edge("find", END) + spine = builder.compile() + + await spine.ainvoke({}, config=config, durability="sync") + + assert set(observed) == {"correctness", "security"} + assert all(isinstance(result, FinderOutput) for result in observed.values()), ( + f"observed={observed!r}; logs={caplog.text}" + ) + assert "Ignoring unknown node name find in pending sends" not in caplog.text + assert model.invocations + + @pytest.mark.asyncio async def test_run_stage_logs_diagnostics_when_no_structured_response( caplog: pytest.LogCaptureFixture,