File size: 2,995 Bytes
9f9d3dc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
from __future__ import annotations

import unittest
from unittest.mock import patch

from agents import executor as executor_module
from agents.executor import Executor


class FailingMemory:
    async def save_episode(self, *_args, **_kwargs) -> None:
        raise RuntimeError("memory unavailable")


class RecordingMemory:
    def __init__(self) -> None:
        self.calls = 0

    async def save_episode(self, *_args, **_kwargs) -> None:
        self.calls += 1


class ExecutorSideEffectRetryTests(unittest.IsolatedAsyncioTestCase):
    async def test_memory_failure_does_not_retry_completed_side_effect(self) -> None:
        calls = 0

        async def non_idempotent_tool(**_inputs):
            nonlocal calls
            calls += 1
            return {"created_id": "resource-1"}

        with patch.dict(
            executor_module.TOOL_REGISTRY,
            {"non_idempotent_tool": {"required_inputs": [], "fallbacks": [], "_fn": non_idempotent_tool}},
            clear=False,
        ):
            result = await Executor(llm_client=object(), memory=FailingMemory(), max_retries=2).run_tool(
                "non_idempotent_tool", {}, timeout=2, worker_hint="test",
            )

        self.assertTrue(result["success"])
        self.assertEqual(result["attempt"], 1)
        self.assertEqual(calls, 1)
        self.assertFalse(result["memory_persisted"])
        self.assertIn("memory unavailable", result["memory_error"])

    async def test_tool_failure_still_retries_before_any_side_effect_result(self) -> None:
        calls = 0

        async def flaky_tool(**_inputs):
            nonlocal calls
            calls += 1
            if calls == 1:
                raise RuntimeError("transient tool failure")
            return {"ok": True}

        with patch.dict(
            executor_module.TOOL_REGISTRY,
            {"flaky_tool": {"required_inputs": [], "fallbacks": [], "_fn": flaky_tool}},
            clear=False,
        ):
            result = await Executor(llm_client=object(), memory=None, max_retries=2).run_tool(
                "flaky_tool", {}, timeout=2, worker_hint="test",
            )

        self.assertTrue(result["success"])
        self.assertEqual(result["attempt"], 2)
        self.assertEqual(calls, 2)

    async def test_successful_memory_persistence_is_reported(self) -> None:
        memory = RecordingMemory()

        async def safe_tool(**_inputs):
            return "done"

        with patch.dict(
            executor_module.TOOL_REGISTRY,
            {"safe_tool": {"required_inputs": [], "fallbacks": [], "_fn": safe_tool}},
            clear=False,
        ):
            result = await Executor(llm_client=object(), memory=memory, max_retries=0).run_tool(
                "safe_tool", {}, timeout=2, worker_hint="test",
            )

        self.assertTrue(result["success"])
        self.assertTrue(result["memory_persisted"])
        self.assertEqual(memory.calls, 1)


if __name__ == "__main__":
    unittest.main()