Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1398,7 +1398,12 @@ async def run_state_creation_workflow(
"total_input_tokens": run_context.total_input_tokens,
"total_output_tokens": run_context.total_output_tokens,
"log_dir": run_context.log_dir,
"fail_count": run_context.fail_count,
# Pass 0 so the sub-workflow reports a per-action delta (0/1),
# not the running cumulative total. The parent accumulates it
# at the completion site; passing run_context.fail_count here
# caused the total to be double-counted (and inflated under
# parallelism).
"fail_count": 0,
},
)

Expand Down
72 changes: 72 additions & 0 deletions deepsearch/tests/search_agent/test_integration_search_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@
pytestmark = pytest.mark.integration


class ExposedDeepSearchAgent(DeepSearchAgent):
"""用于测试的类,公开受保护的方法以遵循 G.CLS.11 规则"""

async def run_internal(self, *args, **kwargs):
return await super()._run_internal(*args, **kwargs)


def _make_agent(tmp_log_dir: Path, **pqp_updates: Any):
agent = DeepSearchAgent()
agent_config = AgentConfig()
Expand Down Expand Up @@ -244,6 +251,71 @@ async def _fake_state_creation(*args: Any, **kwargs: Any) -> SimpleNamespace:
assert final.prediction == "Lyon"


@pytest.mark.asyncio
async def test_fail_count_accumulates_one_per_failed_action(
monkeypatch: pytest.MonkeyPatch, tmp_log_dir: Path, base_action, base_state
) -> None:
"""Each failed action must raise the global fail_count by exactly 1.

Regression for the double-counting bug: the parent used to pass its
cumulative ``fail_count`` into every state_creation sub-workflow AND add the
(already-cumulative) returned value back with ``+=``. With ``fail_limit=3``
that tripped termination after 2 failed actions instead of 3. Here the mock
reproduces the real sub-workflow contract -- it returns the passed-in
``fail_count`` incremented by 1 -- so the bug would surface as an early
termination.
"""
_, run_context = _make_agent(tmp_log_dir, fail_limit=3, max_workers=1)
agent = ExposedDeepSearchAgent()
actions = [
base_action.model_copy(
update={"id": f"action-{i}", "proposal": ActionProposal(direction=f"d{i}", score=0.5)}
)
for i in range(5)
]
state_creation_calls: list[int] = []

async def _fake_run_workflow(*, workflow: str, inputs: dict) -> SimpleNamespace:
if workflow == "init_state_1":
return SimpleNamespace(
result={"init_state": base_state, "total_input_tokens": 0, "total_output_tokens": 0}
)
if workflow == "find_action_1":
return SimpleNamespace(
result={"actions": actions, "total_input_tokens": 0, "total_output_tokens": 0}
)
if workflow == "state_creation_1":
# Mirror algorithm/search_nodes/utils.py: the sub-workflow increments
# the fail_count it was handed and echoes it back inside ``config``.
passed_in = inputs.get("fail_count", 0)
state_creation_calls.append(passed_in)
return SimpleNamespace(
result={
"result": None,
"config": {"fail_count": passed_in + 1},
"total_input_tokens": 0,
"total_output_tokens": 0,
}
)
raise AssertionError(workflow)

monkeypatch.setattr(
"openjiuwen_deepsearch.framework.openjiuwen.agent.workflow.Runner.run_workflow",
_fake_run_workflow,
)

final = await agent.run_internal(run_context)

assert final.termination == "fail_limit"
# Exactly 3 failed actions are needed to reach fail_limit=3 (one increment
# each). The double-counting bug would terminate after only 2.
assert len(state_creation_calls) == 3
assert run_context.fail_count == 3
# The parent must hand a per-action delta base of 0 to each sub-workflow,
# not its running cumulative total.
assert state_creation_calls == [0, 0, 0]


@pytest.mark.asyncio
async def test_answer_writes_final_result_json(
monkeypatch: pytest.MonkeyPatch, tmp_log_dir: Path, base_action, base_state
Expand Down