Since it seems to be an academic exercise, here's an alternative in a different style using patterns and replacements:
ClearAll[findroot, init, left, right, done];
init[f_] := {
{a_, 0, b_, _} :> a, (* done *)
{a_, _, b_, 0} :> b, (* done *)
{a_, sa_, b_, sb_} :> ({a, sa, b, sb, #, Sign@f[#]} &[(a + b)/2])};
left[f_] := {a_, sa_, b_, sb_, c_, sc_} /; sa*sc == -1 :>
({a, sa, c, sc, #, Sign@f[#]} &[(a + c)/2]);
right[f_] := {a_, sa_, b_, sb_, c_, sc_} /; sb*sc == -1 :>
({c, sc, b, sb, #, Sign@f[#]} &[(b + c)/2]);
done[] = {a_, _, b_, _, c_, sc_} /; a === b || sc == 0 :> c;
findroot[func : (_Symbol | _Function), {a_, b_}, eps_: $MachineEpsilon] /; eps > 0 :=
With[{obj = Chop[func[#], eps] &},
{a, Sign@obj@a, b, Sign@obj@b} /. init[obj] //. {done[], left[obj], right[obj]}
];
Note: It has some checks missing in the OP. For instance, init[] checks whether the initial values a or b are roots; done[] stops when the interval {a, b} cannot be subdivided further (well, up to Internal`$SameQTolerance).
Furthermore, if func at the initial {a, sa, b, sb, c, sc} has all the same signs at a/b/c, it will return the data structure showing the same signs. (One could catch this and throw an error.) Otherwise, after done[] checks whether c is a solution, the replacement rule left[] subdivides the left half, and right[] subdivides the right half. I used Chop via obj to implement the tolerance eps. It also never re-evaluates the function at the same point.
Examples:
findroot[Cos, {1., 2.}] (* uses default eps = $MachineEpsilon *)
% - Pi/2
(*
1.5707963267948966`
0.
*)
findroot[Cos, {1., 2.}, 0.001]
% - Pi/2
(*
1.5703125`
-0.000483827
*)
f[x_] := x^2 - 3;
findroot[f, {1., 3.}, 0.00001]
% - Sqrt[3]
(*
1.73205
-2.7729*10^-6
*)