# How aisuite's Agent and Runner Classes Work Together for Structured Agent Workflows

> Understand how aisuite's Agent and Runner classes create structured agent workflows. Learn how configuration objects and execution engines manage LLM calls and conversations.

- Repository: [Andrew Ng/aisuite](https://github.com/andrewyng/aisuite)
- Tags: internals
- Published: 2026-08-03

---

**The `Agent` class is a declarative configuration object that defines model, instructions, and tools, while the `Runner` class provides the execution engine that handles LLM calls, tracing, and stateful multi-turn conversations.**

aisuite's agent framework separates workflow *definition* from workflow *execution*. The `Agent` dataclass in [`aisuite/agents/types.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/types.py) captures static configuration—model name, system instructions, tool callables, and metadata—without any runtime logic. The `Runner` class in [`aisuite/agents/runner.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/runner.py) transforms that configuration into traced, observable, and resumable conversations. This design enables clean, structured agent workflows that can be persisted across turns and integrated with external tracing systems.

## Core Architecture: Agent as Data, Runner as Engine

### Agent: Pure Configuration with No Runtime Logic

The `Agent` class is intentionally minimal. It stores:

- `name` – identifier for tracing and debugging
- `model` – provider-qualified model string (e.g., `"openai:gpt-4o"`)
- `instructions` – system prompt injected at conversation start
- `tools` – list of callable Python functions for tool use
- `model_settings` – provider-specific parameters (temperature, max_tokens, etc.)
- `tags` – static metadata for filtering traces

As implemented in [`aisuite/agents/types.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/types.py) (lines 34-44), `Agent` contains no methods for execution. This separation allows agents to be defined in configuration files, shared across processes, or serialized without coupling to runtime state.

### Runner: The Orchestration Layer

`Runner` provides two public entry points:

- **`run_sync(agent, input, **kwargs)`** – blocking execution
- **`run(agent, input, **kwargs)`** – async execution

Both delegate to the private `_run_impl` method (lines 62-100 in [`aisuite/agents/runner.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/runner.py)), which implements the full agent loop.

## Inside the Execution Flow: Seven Steps from Input to Result

### Step 1: Input Normalization and Message Building

`_run_impl` accepts three input types:

1. Raw string – converted to single user message
2. List of message dicts – used as-is with system prompt prepended
3. `RunState` – deserialized from previous turn via `state_store`

If a string or list is provided, `_build_messages` (lines 62-73) injects `agent.instructions` as the first system message:

```python
import aisuite as ai

def get_weather(city: str) -> str:
    return f"The weather in {city} is sunny."

agent = ai.Agent(
    name="weather_assistant",
    model="openai:gpt-4o",
    instructions="Answer briefly. Use tools when they help.",
    tools=[get_weather],
    model_settings={"temperature": 0.2},
)

result = ai.Runner.run_sync(
    agent,
    "What is the weather in Paris?",  # ← string input becomes message list

    max_turns=3,
    run_name="weather_demo",
)

```

### Step 2: Active Context Propagation

Before any LLM call, `Runner` establishes an `ActiveRunContext` via `set_active_run_context` (lines 40-53). This context contains:

- The active client instance
- Trace ID for the current run
- Agent name and run name
- References to trace sinks

Tool implementations and tracing hooks access this context to correlate operations with the active run without explicit parameter passing.

### Step 3: LLM Invocation with Provider Abstraction

The runner calls `client.chat.completions.create` (sync) or `acreate` (async), using aisuite's unified client interface. The client handles provider-specific authentication, request formatting, and response parsing transparently.

### Step 4: Comprehensive Event Tracing

Throughout execution, `Runner` emits structured trace events (lines 206-226):

| Event | Emitted When |
|-------|--------------|
| `run.started` | Run begins |
| `model.send` | Request dispatched to LLM |
| `model.response` | Response received |
| `tool.called` | Tool execution initiated |
| `tool.result` | Tool execution completes |
| `run.completed` | Final result produced |

Events are delivered to all configured `TraceSink` implementations—console loggers, file writers, or external observability platforms.

### Step 5: Step Construction and Result Assembly

After the LLM response, `Runner` constructs `RunStep` objects (lines 95-115 and 473-508):

- **Agent step** – captures the request configuration
- **Model-response steps** – each intermediate LLM output, including tool calls
- **Tool-call steps** – function calls the model requested
- **Tool-result steps** – actual return values from tool execution

These steps populate a `RunResult` containing the final output, full message history, and structured trace data. The `print_trace()` method formats this for debugging:

```python
result.print_trace()  # Human-readable step-by-step output

```

### Step 6: State Serialization for Persistence

`RunResult.to_state()` produces a `RunState`—a serializable, JSON-friendly representation of the conversation. This enables:

- Cross-process conversation resumption
- Database persistence via `StateStore` implementations
- Audit logging and compliance tracking

### Step 7: Continuation for Multi-Turn Workflows

The `continue_sync` and `continue_run` methods (lines 384-426) reload a `RunState`, append a new user message, and invoke `_run_impl` again. This pattern supports structured multi-turn conversations without manual state management:

```python

# First turn creates persisted thread

first = ai.Runner.run_sync(
    agent, 
    "Weather in New York?", 
    thread_id="demo-1", 
    state_store=my_store
)

# Second turn continues same thread automatically

second = ai.Runner.continue_sync(
    first, 
    "And in Boston?", 
    thread_id="demo-1", 
    state_store=my_store
)
second.print_trace()

```

## Key Files and Their Responsibilities

| File | Lines | Responsibility |
|------|-------|----------------|
| [`aisuite/agents/types.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/types.py) | 34-44 | `Agent` dataclass and core data models (`RunState`, `RunResult`) |
| [`aisuite/agents/runner.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/runner.py) | 40-53 | Context setup and `ActiveRunContext` management |
| [`aisuite/agents/runner.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/runner.py) | 62-100 | Main execution loop (`_run_impl`, `run_sync`, `run`) |
| [`aisuite/agents/runner.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/runner.py) | 206-226 | Trace event emission |
| [`aisuite/agents/runner.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/runner.py) | 384-426 | Continuation logic (`continue_sync`, `continue_run`) |
| [`examples/agents/simple_agent.py`](https://github.com/andrewyng/aisuite/blob/main/examples/agents/simple_agent.py) | Full file | End-to-end demonstration of sync runs, tracing, and continuation |

## Design Patterns in aisuite's Agent Framework

**Declarative Configuration** – Agents are pure data, enabling version control, static analysis, and configuration-driven deployments.

**Context Propagation** – Implicit context storage avoids plumbing trace IDs through every function call while maintaining observability.

**Event-Driven Tracing** – Structured events decouple execution from observability, allowing multiple sinks without code changes.

**State Machine Continuation** – Explicit `RunState`/`RunResult` contracts make conversation boundaries clear and testable.

## Summary

- **`Agent`** defines workflow configuration (model, instructions, tools) as a declarative dataclass in [`aisuite/agents/types.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/types.py)
- **`Runner`** executes workflows through `run_sync`/`run`, delegating to `_run_impl` in [`aisuite/agents/runner.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/runner.py)
- **Input handling** normalizes strings, lists, or `RunState` into message lists with system prompt injection
- **Context propagation** via `ActiveRunContext` enables tool calls and tracing without explicit parameter passing
- **Trace events** (`run.started`, `model.send`, `tool.called`, etc.) provide structured observability
- **Step construction** produces `RunResult` with full execution history and `RunState` for serialization
- **Continuation methods** (`continue_sync`, `continue_run`) enable stateful multi-turn conversations via `StateStore`

## Frequently Asked Questions

### How do I persist an agent conversation across process restarts?

Call `RunResult.to_state()` to obtain a serializable `RunState`, then store it via any `StateStore` implementation. On restart, pass that state as the `input` parameter to `Runner.run_sync` or reload via `continue_sync` with `thread_id` and `state_store` parameters. The `RunState` captures the complete message history, tool results, and metadata needed to resume exactly where the conversation left off.

### What is the difference between `run_sync` and `continue_sync`?

`run_sync` starts a new conversation turn from an `Agent` plus string/list/`RunState` input, optionally creating a new `thread_id`. `continue_sync` specifically resumes an existing conversation: it accepts a `RunResult` from the previous turn, appends your new message, and preserves the same `thread_id`. Use `run_sync` for fresh interactions or when managing state manually; use `continue_sync` for natural multi-turn flows with automatic state propagation.

### Can I use the same `Agent` instance across multiple concurrent runs?

Yes. Since `Agent` contains no mutable runtime state, it is safe to share across threads or async tasks. Each `run_sync` or `run` call creates isolated `ActiveRunContext` and trace ID, ensuring concurrent executions do not interfere. Define your `Agent` once at module level or in a configuration file, then invoke it from any number of concurrent workflows.

### How are tool call results injected back into the conversation?

When the LLM response includes tool calls, `Runner` executes the matching functions from `agent.tools`, captures return values, and automatically appends `tool` role messages to the conversation history. These results are included in the next LLM request within the same `max_turns` budget, allowing the model to incorporate tool outputs without manual message construction. The full tool-call and tool-result steps appear in `RunResult.steps` and any configured trace sinks.