Repository: UKGovernmentBEIS/inspect_ai
Issue: basic_agent initialise infinite tools when being wrapped in an orchestrator for multi-agent evals
Root cause: Architectural Constraint

# Basic agent tool duplication in orchestrator conversation loops
## Issue Description
When running the basic agent inside an orchestrator that manages a conversation loop between two agents (attacker and monitor), we encounter an error after multiple iterations. The problem occurs because:

1. The agent is initialized every time a conversation-pair between these agents finishes.
2. The submit tool is mandatory and must be returned each initialization.
3. After several iterations, we get an error in Inspect that too many tools have been passed.
4. The submission tool is infinitely being generated

## Reproduction Steps

1. Create an orchestrator custom solver: orchestrator(attack_solver, monitor_solver).
2. The attacker creates a submission to trick the monitor.
3. The monitor replies with a rating about how suspicious the submission is.
4. After iteration #1 is completed, the attacker creates another submission for iteration #2.
5. This process continues repeating.
6. Eventually, an error occurs due to tool duplication.

## Technical Details
The orchestrator creates a chain with the submission tool and system prompts for both agents. The current implementation of basic_agent mandatorily initializes submission tools with each loop, which is problematic in this orchestrated environment.

## Proposed Solution
Easy fix: adding a parameter only_loop: bool to the basic agent that, when set to True, returns the basic_agent_loop; like this we can wrap it properly in an orchestrator containing tools a level above. 
## Additional Context
This issue impacts control evaluation workflows that require multiple iterations of agent interactions.


## Screenshots (The tools are being called in the orchestator):

### Before
![Image](https://github.com/user-attachments/assets/5940097c-5b2a-47a7-b612-489497934c40)
![Image](https://github.com/user-attachments/assets/8d978f6b-9719-4f3a-8e9e-9dbfe82f0488)
![Image](https://github.com/user-attachments/assets/319bab75-191a-4288-a9c6-86428b5071e9)
![Image](https://github.com/user-attachments/assets/b3c42463-08a9-4f2c-83ce-0a93117b3114)

### After
![Image](https://github.com/user-attachments/assets/ac793d4b-2be6-4a57-9567-677c8310739c)
![Image](https://github.com/user-attachments/assets/ff103fab-79ab-41c5-9c67-66f7a0429877)

Thanks for writing this up! Could we also potentially address this by changing the semantics of basic_agent to fully replace and initialize `state.tools`? (I can't think of a reason why it shouldn't and it may just be an oversight or result from a tacit assumption that basic_agent is the highest level solver).

You mean changing ```state.tools.append(tool_with(submit(), submit_name, submit_description))``` to ```state.tools = [tool_with(submit(), submit_name, submit_description)]```?

I think the issue happens in the mandatory chain but I could be wrong :/

I can explain to you with some high-level details what I did

```
    return chain(
        [
            initialize_state_solver(),
            system_message(get_system_prompt(role="attacker")),
            use_tools(config.tools),
            submit_tool(),
            workflow_solver(
                create_attack_solver(**config.attacker_kwargs),
                create_monitor_solver(**config.monitor_kwargs),
                max_attempts=max_attempts,
            ),
        ]
    )

```

Essentially, this is a control evaluation for two agents, I am making Agent 1 repeatedly send coding submissions to Agent 2, and then Agent 2 responds with a suspicious score that goes to Agent 1 for feedback, so it can create a more suspicious solution. Both agents call the ```basic_agent``` solver and have ```python```, ```bash``` and ```submit tools``` as they are collaborating in the same environment but Agent 1 has severe restrictions. This is a conversation loop that finishes until Agent 1 runs out of attacking attempts

Okay, I think I understand! We have a PR in flight that is going to introduce a new "Agent" protocol and a re-imagining of `basic_agent()` as `react()` with no coupling to a central `TaskState`. Here is the code for `react()` (it's basically exactly what you are asking for, just the loop):

https://github.com/UKGovernmentBEIS/inspect_ai/blob/feature/generate-loop/src/inspect_ai/agent/_react.py

I think I'd like to resolve this via this new functionality rather than try to hack it in to `basic_agent()`. 


Nice! Will try the new functionality! Is there any way to specify different models when I'm dealing with multiple copies of the react agent? (Without using fork()) 

The use case is for AI debate or multi-agent evals where all agents are react 

Context:

To count for cases where I want the monitor to be a different model than the attacker, I created another hacky fix to this by adding ```model: str```,  so it can be passed down and used in ```get_model()```

Imagine the final expression is something like: ``` solver = await orchestrator(attacker_solver("gpt-4o"), monitor_solver("gpt-3.5-turbo"))```

Both attacker and monitor are wrappers of ```basic_agent``` but since the solver always calls ```get_model()``` it keeps calling the model specified on inspect or in ```--model```



The `react()` agent has a `model` parameter that you can use to vary the model by agent. I think all of this should be resolved by using the `react()` agent.