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 debuggingmodel– provider-qualified model string (e.g.,"openai:gpt-4o")instructions– system prompt injected at conversation starttools– list of callable Python functions for tool usemodel_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 executionrun(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:
- Raw string – converted to single user message
- List of message dicts – used as-is with system prompt prepended
RunState– deserialized from previous turn viastate_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
StateStoreimplementations - 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
Agentdefines workflow configuration (model, instructions, tools) as a declarative dataclass inaisuite/agents/types.pyRunnerexecutes workflows throughrun_sync/run, delegating to_run_implinaisuite/agents/runner.py- Input handling normalizes strings, lists, or
RunStateinto message lists with system prompt injection - Context propagation via
ActiveRunContextenables tool calls and tracing without explicit parameter passing - Trace events (
run.started,model.send,tool.called, etc.) provide structured observability - Step construction produces
RunResultwith full execution history andRunStatefor serialization - Continuation methods (
continue_sync,continue_run) enable stateful multi-turn conversations viaStateStore
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →