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

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 captures static configuration—model name, system instructions, tool callables, and metadata—without any runtime logic. The Runner class in 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 (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), 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:

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:

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:


# 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 34-44 Agent dataclass and core data models (RunState, RunResult)
aisuite/agents/runner.py 40-53 Context setup and ActiveRunContext management
aisuite/agents/runner.py 62-100 Main execution loop (_run_impl, run_sync, run)
aisuite/agents/runner.py 206-226 Trace event emission
aisuite/agents/runner.py 384-426 Continuation logic (continue_sync, continue_run)
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
  • Runner executes workflows through run_sync/run, delegating to _run_impl in 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.

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 →