# Understanding the GenericAgent Agent Loop Architecture and Implementation

> Explore the GenericAgent Agent Loop architecture. Learn how this generator-driven system orchestrates LLM conversations and tool calls for efficient execution.

- Repository: [LJQ/GenericAgent](https://github.com/lsdefine/GenericAgent)
- Tags: architecture
- Published: 2026-04-16

---

**The GenericAgent Agent Loop is a turn-based, generator-driven orchestration system that continuously builds LLM conversations, dispatches tool calls to handlers, and yields execution status until completion or maximum turn limits.**

The GenericAgent repository by lsdefine implements a modular architecture for autonomous LLM agents. At its core lies the **GenericAgent Agent Loop**, a deterministic generator that manages the conversation flow between the language model and tool implementations. This architecture cleanly separates orchestration logic from tool execution and LLM client abstraction.

## Core Components of the GenericAgent Agent Loop

The architecture consists of four primary components that work together to create a deterministic, extensible execution environment.

### agent_runner_loop Generator

The **`agent_runner_loop`** generator function, defined in [`agent_loop.py`](https://github.com/lsdefine/GenericAgent/blob/main/agent_loop.py), serves as the central orchestration engine. It implements a `while` loop that continues until the handler signals completion or the system reaches the maximum turn count. The generator yields human-readable status strings such as "LLM Running (Turn 3)…" and tool execution logs, enabling real-time streaming to user interfaces.

### BaseHandler and Dispatch Mechanism

**`BaseHandler`**, located at lines 14-30 in [`agent_loop.py`](https://github.com/lsdefine/GenericAgent/blob/main/agent_loop.py), provides the abstract foundation for all tool implementations. It defines the **`dispatch`** method that dynamically routes tool calls to concrete handler methods using the naming convention `do_<tool_name>`. For example, a tool named `code_run` dispatches to `do_code_run`. This reflection-based approach allows new tools to be added without modifying the loop logic.

### StepOutcome Data Structure

The **`StepOutcome`** class, defined at lines 5-9 in [`agent_loop.py`](https://github.com/lsdefine/GenericAgent/blob/main/agent_loop.py), encapsulates the result of each turn. It carries three critical pieces of information: the tool execution data, the next prompt content for the following turn, and a boolean flag indicating whether the loop should terminate. When the next prompt is `None` or the handler sets `should_exit` to `True`, the generator concludes its execution.

## How the GenericAgent Agent Loop Executes

The loop follows a strict six-step execution pattern that maintains deterministic state management across turns.

**Step 1:** The loop constructs the LLM conversation by combining the system prompt with the current user input and accumulated conversation history.

**Step 2:** It calls the LLM client—accessed via `client.chat` from [`llmcore.py`](https://github.com/lsdefine/GenericAgent/blob/main/llmcore.py)—passing the message history and the JSON-encoded **tool schema** loaded from [`assets/tools_schema.json`](https://github.com/lsdefine/GenericAgent/blob/main/assets/tools_schema.json).

**Step 3:** The system parses the LLM response. If the model returns a function call, the loop extracts the specific tool name and arguments using the schema definitions.

**Step 4:** The loop dispatches the call via `BaseHandler.dispatch`, which locates the corresponding `do_<tool_name>` method in the concrete handler—typically `GenericAgentHandler` implemented in [`ga.py`](https://github.com/lsdefine/GenericAgent/blob/main/ga.py). This handler executes the actual logic, whether running code, reading files, or driving a browser.

**Step 5:** The loop collects the tool result from the returned `StepOutcome`, checking for early exit signals such as `should_exit` or a `None` next prompt. It then constructs the next prompt containing updated working memory, recent turn history, and any system warnings.

**Step 6:** The loop feeds the next prompt back into the message list and repeats. Execution terminates when the handler signals `should_exit`, the tool returns a final `next_prompt` of `None`, or the turn counter exceeds the maximum—defaulting to 40 turns or extending to 80 in plan mode.

## Implementation Details: File Structure and Key Functions

The GenericAgent Agent Loop implementation spans six critical files that enforce separation of concerns between orchestration, tool execution, and LLM abstraction.

| File | Role | Key Implementation Details |
|------|------|----------------------------|
| [`agent_loop.py`](https://github.com/lsdefine/GenericAgent/blob/main/agent_loop.py) | Core orchestration logic | Contains `agent_runner_loop` generator, `BaseHandler` class (lines 14-30), and `StepOutcome` dataclass (lines 5-9). |
| [`agentmain.py`](https://github.com/lsdefine/GenericAgent/blob/main/agentmain.py) | High-level entry point | Implements `GeneraticAgent.run()` (lines 99-138), handles LLM client selection via `next_llm()`, loads tool schemas via `load_tool_schema()`, and manages the UI display queue. |
| [`ga.py`](https://github.com/lsdefine/GenericAgent/blob/main/ga.py) | Concrete tool implementations | Defines `GenericAgentHandler` with all `do_*` methods (e.g., `do_code_run`, `do_file_read`) that execute actual operations and return `StepOutcome` objects. |
| [`llmcore.py`](https://github.com/lsdefine/GenericAgent/blob/main/llmcore.py) | LLM client abstraction | Provides `ToolClient`, `ClaudeSession`, and other session classes that implement the `chat(messages, tools)` interface consumed by the loop. |
| [`assets/tools_schema.json`](https://github.com/lsdefine/GenericAgent/blob/main/assets/tools_schema.json) | Tool definitions | JSON schema describing available tools, their parameters, and descriptions; loaded at runtime to construct the LLM function-calling context. |
| `frontends/*.py` | UI integrations | Platform-specific implementations (e.g., [`tgapp.py`](https://github.com/lsdefine/GenericAgent/blob/main/tgapp.py) for Telegram) that queue tasks and stream the generator output to chat interfaces. |

## Practical Code Examples

These examples demonstrate how to interact with the GenericAgent Agent Loop at different levels of abstraction.

### Running the Loop Manually

To execute the loop outside the full agent context for testing or debugging:

```python
from llmcore import ToolClient, ClaudeSession
from agent_loop import agent_runner_loop, StepOutcome
from ga import GenericAgentHandler
import json, os

# Load the tool schema from assets

with open(os.path.join('assets', 'tools_schema.json'), encoding='utf-8') as f:
    TOOLS_SCHEMA = json.load(f)

# Initialize the LLM client

client = ToolClient(ClaudeSession(cfg={'api_key': 'YOUR_KEY'}))

# Configure prompts

system_prompt = "You are a helpful assistant."
user_input = "List all .py files in the repository."

# Create handler instance

handler = GenericAgentHandler(parent=None)

# Execute the generator and stream output

for chunk in agent_runner_loop(
        client, system_prompt, user_input,
        handler, TOOLS_SCHEMA, max_turns=5, verbose=True):
    print(chunk, end='')

```

This approach mirrors the internal logic of `GeneraticAgent.run()` while providing isolation for unit testing.

### Adding a Custom Tool

Extending the agent with new capabilities requires implementing a handler method and updating the schema:

```python

# In ga.py, add to GenericAgentHandler

def do_git_branch(self, args, response):
    import subprocess
    branch = subprocess.check_output(
        ['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
        text=True
    ).strip()
    return StepOutcome(
        {"branch": branch},
        next_prompt=self._get_anchor_prompt()
    )

```

After adding the corresponding entry to [`assets/tools_schema.json`](https://github.com/lsdefine/GenericAgent/blob/main/assets/tools_schema.json), the loop automatically routes `git_branch` calls to this method without modifying the core orchestration logic.

### Integrating with a Web Front-end

The generator design enables seamless UI integration. This simplified example from the Telegram frontend demonstrates the pattern:

```python
from agentmain import GeneraticAgent

# Initialize the agent

bot = GeneraticAgent()
bot.next_llm(0)  # Select first LLM configuration

bot.verbose = False  # Disable console output for chat UI

# The UI layer pushes user messages via bot.put_task()

# and streams bot.run() output back to the chat client

```

The front-end merely queues tasks and consumes the display queue; the GenericAgent Agent Loop handles the complex orchestration of LLM calls and tool execution.

## Summary

The GenericAgent Agent Loop architecture provides a robust, generator-based framework for autonomous LLM agents with these key characteristics:

- **Generator-driven execution** enables real-time streaming of turn status and tool execution logs through the `agent_runner_loop` generator in [`agent_loop.py`](https://github.com/lsdefine/GenericAgent/blob/main/agent_loop.py).
- **Reflection-based dispatch** allows dynamic tool routing via `BaseHandler.dispatch`, which automatically maps LLM function calls to `do_<tool_name>` methods in `GenericAgentHandler`.
- **Deterministic termination** occurs through explicit signals (`should_exit`, `None` next prompt) or configurable turn limits (default 40, 80 in plan mode).
- **Clean separation of concerns** between orchestration ([`agent_loop.py`](https://github.com/lsdefine/GenericAgent/blob/main/agent_loop.py)), LLM abstraction ([`llmcore.py`](https://github.com/lsdefine/GenericAgent/blob/main/llmcore.py)), tool implementation ([`ga.py`](https://github.com/lsdefine/GenericAgent/blob/main/ga.py)), and entry points ([`agentmain.py`](https://github.com/lsdefine/GenericAgent/blob/main/agentmain.py)).

## Frequently Asked Questions

### How does the GenericAgent Agent Loop handle tool execution errors?

The loop captures exceptions within the `do_<tool_name>` methods implemented in [`ga.py`](https://github.com/lsdefine/GenericAgent/blob/main/ga.py). When an error occurs, the handler returns a `StepOutcome` containing the error message in the data field, which the loop incorporates into the next prompt as context for the LLM. This allows the agent to reason about failures and potentially retry or adjust its approach in subsequent turns.

### What is the maximum number of turns the GenericAgent Agent Loop can execute?

By default, the loop enforces a maximum of **40 turns** before automatically terminating to prevent infinite loops. When operating in plan mode, this limit extends to **80 turns** to accommodate complex multi-step reasoning. These limits are configurable through the `max_turns` parameter passed to `agent_runner_loop` or set on the handler instance.

### Can the GenericAgent Agent Loop work with different LLM providers?

Yes, the architecture abstracts LLM interactions through the `ToolClient` interface defined in [`llmcore.py`](https://github.com/lsdefine/GenericAgent/blob/main/llmcore.py). The loop accepts any client implementing the `chat(messages, tools)` method, including `ClaudeSession` for Anthropic models, OpenAI-compatible clients, and native implementations. Swapping providers requires only changing the client instantiation in [`agentmain.py`](https://github.com/lsdefine/GenericAgent/blob/main/agentmain.py) without modifying the core loop logic.

### How does the loop maintain state between consecutive turns?

The loop maintains state through the conversation message list and the `StepOutcome` objects returned by tool handlers. After each turn, the system appends the tool result and next prompt to the message history, which is passed back to the LLM client in the subsequent iteration. The `GenericAgentHandler` may also maintain internal state (such as working memory or file system references) that persists across turns until the loop terminates or the handler signals an exit.