# How the Runner Class Executes Agent Workflows in aisuite: A Complete Technical Guide

> Understand how the Runner class executes agent workflows in aisuite. Learn about input normalization, model calls, tool interactions, trace events, and state persistence.

- Repository: [Andrew Ng/aisuite](https://github.com/andrewyng/aisuite)
- Tags: deep-dive
- Published: 2026-07-30

---

**The `Runner` class orchestrates end-to-end agent workflows by normalizing inputs, executing async or sync model calls, handling tool interactions, emitting detailed trace events, and persisting conversation state.**

The `Runner` class in the `andrewyng/aisuite` repository serves as the central orchestrator that drives agent execution from prompt preparation through final output delivery. It abstracts away the complexity of managing LLM provider interactions, tool calling loops, and telemetry collection into a unified API. Understanding how the `Runner` class executes agent workflows reveals the architectural patterns that enable robust, observable, and stateful AI agent systems.

## Execution Pipeline: From Input to Result

The execution flow in [`aisuite/agents/runner.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/runner.py) proceeds through twelve distinct stages that handle everything from input validation to final state persistence.

### Initialization and Input Processing

Execution begins when users invoke **`Runner.run()`** (async) or **`Runner.run_sync()`** (sync). Both entry points delegate to the private **`_run_impl`** method, which serves as the unified execution engine.

Input normalization distinguishes between raw inputs (`str` or list) and pre-built **`RunState`** objects. The method **`_build_messages`** constructs the initial message list and injects system instructions according to the agent's configuration. Any stored **artifacts**—such as file references from previous turns—are rehydrated into the message payload via `hydrate_messages` to restore the full conversation context.

### Request Configuration and Telemetry Setup

Before invoking the model, the runner merges configuration hierarchies. It combines `agent.model_settings` with user-provided `kwargs`, attaches available tools if defined, and sets `max_turns` to limit conversation length. An optional **`tool_policy`** can be injected to control tool execution behavior.

Tracing initialization generates a unique trace ID unless telemetry is disabled. The runner emits a `run.started` event and, for providers that don't emit native model events, a `model.send` event to all configured **`TraceSink`** instances. The **`set_active_run_context`** method then stores the current run context—including the client, trace ID, and agent name—enabling downstream utilities and tool wrappers to access execution metadata globally.

### Model Execution and Response Handling

Depending on the `use_async_client` flag, the runner invokes either `client.chat.completions.acreate` (async) or `.create` (sync) with the prepared message payload. The implementation in `_run_impl` wraps these calls in comprehensive error handling to emit `model.error` and `run.failed` events when exceptions occur.

Upon successful completion, the response undergoes normalization. The runner constructs **`RunStep`** objects representing both the agent's output and the model's response, transforming any embedded tool events into distinct steps. This structured approach enables detailed inspection of the execution trajectory.

### Finalization and State Management

The runner assembles a **`RunResult`** object containing the final output, raw provider responses, the updated message list, complete step history, and trace metadata. It emits `model.response` (if not already provided by the underlying provider) and iterates through tool events to emit `tool.allowed`, `tool.completed`, and related lifecycle events before finally signaling `run.completed`.

If a **`StateStore`** is supplied, the result undergoes dehydration—stripping artifact references to create a serializable snapshot—and persists under the specified `thread_id`. The **cleanup phase** ensures `reset_active_run_context` runs in a `finally` block, guaranteeing context isolation even when errors occur.

## Key Architectural Components

### Async and Sync Execution Modes

The `Runner` provides dual APIs to accommodate different runtime environments. **`run_sync()`** wraps the async implementation using `asyncio.run()`, making it suitable for scripts and notebooks, while **`run()`** supports native async/await patterns for high-concurrency applications.

### Tracing and Observability

Every significant lifecycle event generates structured telemetry. The runner emits events for run start/completion, model requests/responses, tool executions, and error conditions. These events flow to configurable sinks defined in [`aisuite/tracing/sinks.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/tracing/sinks.py), enabling integration with monitoring systems and debugging tools.

### State Persistence Architecture

The **`StateStore`** abstraction in [`aisuite/mcp/state_store.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/mcp/state_store.py) enables conversation continuity across process boundaries. By dehydrating large artifacts (managed in [`aisuite/agents/artifact_store.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/artifact_store.py)) and storing only references, the runner optimizes storage while maintaining the ability to reconstruct full conversation state.

## Practical Code Examples

### Basic Async Execution

```python
from aisuite.agents.runner import Runner

async def execute_agent():
    result = await Runner.run(
        agent=my_agent,
        input="Analyze the quarterly sales data",
        max_turns=5,
    )
    print(result.final_output)

```

### Synchronous Execution for Scripts

```python
from aisuite.agents.runner import Runner

def quick_summary():
    result = Runner.run_sync(
        agent=my_agent,
        input="Summarize this article: ...",
        tracing_disabled=True,
    )
    return result.final_output

```

### Continuing a Persisted Conversation

```python
from aisuite.agents.runner import Runner
from aisuite.mcp.state_store import InMemoryStateStore

store = InMemoryStateStore()
thread_id = "user-session-123"

# Initial execution

first_result = Runner.run_sync(
    agent=my_agent,
    input="Initialize the analysis",
    state_store=store,
    thread_id=thread_id
)

# Resume later

continuation = Runner.run_sync(
    agent=my_agent,
    input="Provide more details on the second point",
    state_store=store,
    thread_id=thread_id
)

```

## Core Files in the Execution Pipeline

| File | Role |
|------|------|
| [`aisuite/agents/runner.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/runner.py) | Main orchestrator containing `Runner` class and `_run_impl` logic |
| [`aisuite/agents/types.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/types.py) | Defines `Agent`, `RunResult`, `RunState`, and `RunStep` dataclasses |
| [`aisuite/client.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/client.py) | LLM provider wrapper; instantiated by `Runner` when no client is provided |
| [`aisuite/tracing/sinks.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/tracing/sinks.py) | Implements `TraceSink` interface for telemetry emission |
| [`aisuite/mcp/state_store.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/mcp/state_store.py) | Abstract persistence layer for conversation continuity |
| [`aisuite/agents/artifact_store.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/artifact_store.py) | Manages serialization of large objects referenced in messages |

## Summary

- The **`Runner`** class provides unified async (`run`) and sync (`run_sync`) entry points that delegate to `_run_impl` for execution orchestration.
- Input processing involves **`_build_messages`** for system prompt injection and artifact hydration to restore conversation context.
- Model invocation supports both async and sync client modes with comprehensive error handling and automatic trace event emission.
- The execution flow generates detailed **`RunStep`** objects and **`RunResult`** instances containing full conversation history and metadata.
- **State persistence** via `StateStore` enables conversation dehydration/rehydration, while the context management system ensures proper isolation across concurrent runs.

## Frequently Asked Questions

### What is the difference between `run()` and `run_sync()` in the aisuite Runner?

The **`run()`** method is the native async implementation that returns a coroutine and must be awaited, making it ideal for applications handling multiple concurrent agent workflows. The **`run_sync()`** method provides a synchronous wrapper that internally calls `asyncio.run()` on the async implementation, designed for convenience in scripts, notebooks, or legacy synchronous codebases.

### How does the Runner handle tool calling during agent execution?

When an agent defines tools, the `Runner` attaches them to the model request and sets `max_turns` to enable multi-turn conversations. Tool calls embedded in model responses are transformed into **`RunStep`** objects with specific event types (`tool.allowed`, `tool.completed`), allowing the execution loop to continue until all tool interactions resolve or the turn limit is reached.

### Can I persist and resume agent conversations across different processes?

Yes, by supplying a **`StateStore`** implementation (such as `InMemoryStateStore` or a custom database-backed store) and a consistent `thread_id`, the `Runner` automatically dehydrates the conversation state—stripping large artifacts to references—and persists it. Subsequent calls with the same `thread_id` and store instance resume the conversation from the previous state.

### How does tracing work in the Runner class?

The `Runner` initializes a trace ID at execution start and emits structured events including `run.started`, `model.send`, `model.response`, `tool.completed`, and `run.completed` to all configured **`TraceSink`** instances. This occurs throughout the pipeline in [`aisuite/agents/runner.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/runner.py), enabling comprehensive observability without requiring manual instrumentation in agent definitions.