Understanding the GenericAgent Agent Loop Architecture and Implementation

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, 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, 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, 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—passing the message history and the JSON-encoded tool schema loaded from 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. 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 Core orchestration logic Contains agent_runner_loop generator, BaseHandler class (lines 14-30), and StepOutcome dataclass (lines 5-9).
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 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 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 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 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:

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:


# 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, 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:

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.
  • 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), LLM abstraction (llmcore.py), tool implementation (ga.py), and entry points (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. 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. 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →