Phase 3 reference solution

An example scenario for an edge LOOP-1…5 never exercises: malformed JSON in the model’s tool-call arguments, which the docstring contract says must fall back to an empty dict rather than crash. Append to features/agent_loop.feature:

  Scenario: LOOP-6 malformed tool arguments fall back to an empty dict
    Given the scripted model calls tool "travel_time" with malformed arguments, then yields
    When the agent loop runs
    Then the loop ran 2 rounds
    And tool "travel_time" got empty arguments

And the two new steps in features/steps/agent_loop_steps.py (the existing _call helper always emits valid JSON, so build the malformed call directly):

@given('the scripted model calls tool "{name}" with malformed arguments, then yields')
def step_call_malformed(context, name):
    call = SimpleNamespace(
        id="call-1",
        type="function",
        function=SimpleNamespace(name=name, arguments="{not json"),
    )
    context.script = [
        _turn("<think>bad args</think>", [call]),
        _turn("<think>done</think>recovered"),
    ]


@then('tool "{name}" got empty arguments')
def step_empty_args(context, name):
    assert (name, {}) in context.calls, context.calls

Verified: all 6 scenarios pass against the phase-2 reference loop.