Chapter 2 - Your First Agent: Quickstart and the Runner
This article is part of my book Tinib00k: OpenAI Agents SDK, which explores the OpenAI Agents SDK, its primitives, and patterns for building agentic AI systems. All of the chapters can be found here and the code is available on Github. The book is available for free online and as a PDF eBook. If you find this content valuable, please consider supporting my work by purchasing the eBook (or download it for free!) or sharing it with others. For any issues around the book, contact me on LinkedIn
In the previous chapter, we introduced the high-level concepts behind agentic AI. Now, it’s time to translate that theory into practice. This chapter is your hands-on guide to building and executing your first functional agent. We will begin with a foundational “Hello, World” example and then incrementally add complexity by introducing tools. Along the way, we’ll dissect the Runner
class and the Agent Loop—the core engine that brings your agents to life. By the end of this chapter, you will not only have a running agent but also a clear mental model of the entire execution flow.
If you have already followed the steps in Appendix A to run the code examples (highly recommended), you don’t necessarily need to follow all of the following steps. However, it’s a good idea to glance over these sections before jumping to the “Hello, Agent!” section.
Environment Setup
Before we write any code, let’s ensure your development environment is correctly configured. This is a one-time setup for your project.
First, create and activate a Python virtual environment. This isolates your project’s dependencies from other Python projects on your system.
1
2
3
4
5
6
7
# Create the project directory
mkdir my-agent-project
cd my-agent-project
# Create and activate a virtual environment
python -m venv .venv
source .venv/bin/activate
Next, install the necessary packages. We’ll need openai-agents
for the SDK itself and litellm
to interact with Google’s Gemini models.
1
pip install "openai-agents[litellm]"
Finally, you must provide an API key for the model provider you intend to use. Since we are using Gemini in these examples (Read the Preface to understand why), you will need to get a key from Google AI Studio and set it as an environment variable.
1
export GOOGLE_API_KEY="your-api-key-here"
API Keys are Essential
The SDK needs credentials to communicate with LLM providers. If the appropriate environment variable (e.g.,
GOOGLE_API_KEY
,ANTHROPIC_API_KEY
) is not set,litellm
will raise an authentication error when theRunner
attempts to call the model. Always ensure your keys are correctly configured in your terminal session or environment management tool.
Hello, Agent!
With our environment ready, let’s build the simplest possible agent. An agent, at its core, is an LLM configured with a set of instructions. The two fundamental classes we’ll use are Agent
and Runner
.
Agent
: This class defines the agent’s identity and capabilities. We’ll give it aname
, a set ofinstructions
(its system prompt), and tell it whichmodel
to use.Runner
: This is the execution engine. We’ll useRunner.run_sync()
for this first example, which is a convenient synchronous method that runs the agent and waits for the final result.
Here is the complete code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#
# A basic agent that responds to a prompt.
#
from agents import Agent, Runner
from tinib00k.utils import DEFAULT_LLM, load_and_check_keys
load_and_check_keys()
def main():
# 1. Define the Agent
polite_agent = Agent(
name="Polite Assistant",
instructions="You are a helpful and polite assistant. You always answer in a clear and friendly tone.",
model=DEFAULT_LLM
)
# 2. Define the input
user_input = "What is the primary function of a CPU in a computer?"
# 3. Use the Runner to execute the agent
print(f"User: {user_input}")
result = Runner.run_sync(polite_agent, user_input)
# 4. Print the final output
print(f"
Agent: {result.final_output}")
if __name__ == "__main__":
main()
In this example, the Runner
takes our polite_agent
and the user_input
, sends them to the Gemini model, and receives a single, complete response. This single-turn interaction is the most basic form of the Agent Loop.
The result
object returned by the Runner
is an instance of RunResult
. For now, we are only interested in the final_output
property, which contains the agent’s text response. We’ll explore the other properties of RunResult
later in this chapter.
Empowering Agents with Tools
Our first agent was essentially a wrapper around a single LLM call. The true power of agents is unlocked when they can interact with the outside world. We enable this by giving them tools.
In the Agents SDK, any Python function can be turned into a tool using the @function_tool
decorator. The SDK automatically inspects the function’s signature and docstring to create a schema that the LLM can understand and use.
Let’s create a simple tool and give it to a new agent.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#
# An agent that can use a tool to get information.
#
import random
from agents import Agent, Runner, function_tool
from tinib00k.utils import DEFAULT_LLM, load_and_check_keys
load_and_check_keys()
# --- Tool Definition ---
@function_tool
def get_stock_price(symbol: str) -> float:
"""
Gets the current stock price for a given ticker symbol.
Args:
symbol: The stock ticker symbol (e.g., "GOOGL", "AAPL").
"""
print(f"[Tool Executed]: Getting stock price for {symbol}")
# In a real application, this would call a financial data API.
# Here, we'll just return a random price for demonstration.
return round(random.uniform(100, 1000), 2)
# --- Agent Definition ---
def main():
# Define an agent and provide it with the tool
financial_agent = Agent(
name="Financial Assistant",
instructions="You are a financial assistant. Use your tools to answer questions. Be concise.",
model=DEFAULT_LLM,
tools=[get_stock_price] # The tool is passed in a list
)
# Use the Runner to execute the agent
result = Runner.run_sync(
financial_agent,
"What is the current price of Google's stock?"
)
print(f"
Final Answer:
{result.final_output}")
if __name__ == "__main__":
main()
When you run this code, you’ll see the following output:
1
2
3
4
[Tool Executed]: Getting stock price for GOOGL
Final Answer:
The current price of GOOGL is $...
Notice the line [Tool Executed]
appears before the final answer. This is a critical insight into how the Agent Loop works with tools. The agent didn’t know the price of Google’s stock. It reasoned that it needed to use the get_stock_price
tool, the SDK executed that tool, and then the agent used the tool’s output to formulate its final response.
Unpacking the Multi-Turn Agent Loop
The previous example involved a multi-turn interaction with the LLM, all managed seamlessly by the Runner
. Let’s break down the sequence of events:
- Turn 1: Reasoning and Tool Selection
- The
Runner
sends the initial prompt (“What is the current price of Google’s stock?”) and the schema of theget_stock_price
tool to the Gemini model. - The model analyzes the prompt. It recognizes that it cannot answer the question directly but has a tool that can.
- Instead of returning a final text answer, the LLM returns a special message indicating a tool call. It specifies that it wants to call the
get_stock_price
tool with the argumentsymbol="GOOGL"
.
- The
- SDK Action: Tool Execution
- The Agents SDK intercepts this tool call message.
- It looks up the
get_stock_price
function in the agent’stools
list. - It executes the Python function
get_stock_price("GOOGL")
. - The function returns a float (e.g.,
178.45
).
- Turn 2: Synthesizing the Final Answer
- The
Runner
automatically appends the result of the tool call to the conversation history. The history now contains the original user prompt and a new message indicating that theget_stock_price
tool was called and returned178.45
. - It sends this entire updated history back to the Gemini model.
- The model now has all the information it needs. It sees the original question and the data from the tool. It uses this context to generate a natural language response: “The current price of GOOGL is $178.45.”
- This time, the model’s response is a final answer, not a tool call. The Agent Loop has completed its objective and terminates.
- The
This multi-turn process is the essence of how agents reason and act.
Async and Streaming
While run_sync()
is convenient for simple scripts, most real-world applications (like web servers) are asynchronous. The SDK is async-native and provides two other methods on the Runner
:
Runner.run()
: The asynchronous equivalent ofrun_sync()
. You shouldawait
its result. It’s the preferred method for any application usingasyncio
.Runner.run_streamed()
: This method also runs asynchronously but returns aRunResultStreaming
object immediately. You can then iterate over this object’s events to get real-time updates as the LLM generates tokens or calls tools. This is ideal for chatbot UIs where you want to display the response as it’s being generated.
We will cover streaming in depth in a later chapter, but here’s a quick preview:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#
# A quick look at streaming
#
# ... (agent and tool definitions from before) ...
async def stream_example():
# ...
financial_agent = Agent(
# ... same definition
)
result = Runner.run_streamed( # Note: run_streamed
financial_agent,
"What is the current price of Google's stock?"
)
print("Agent's Final Answer: ", end="")
# The stream_events() iterator yields different event types.
# We filter for text deltas to print the response token-by-token.
async for event in result.stream_events():
if event.type == "raw_response_event" and event.data.type == "response.output_text.delta":
print(event.data.delta, end="", flush=True)
print() # for a final newline
# ...
The RunResult
Object
The Runner
returns a rich RunResult
object containing a complete record of the agent’s execution. While final_output
is often all you need, exploring the other properties is crucial for debugging and building complex logic.
result.new_items
: A list of all new events generated during the run, includingMessageOutputItem
,ToolCallItem
, andToolCallOutputItem
. This gives you a structured history of what happened.result.raw_responses
: The raw, unprocessed response objects from the LLM provider.result.last_agent
: A reference to the agent that produced the final output. This is especially important in multi-agent workflows with handoffs.result.to_input_list()
: A convenience method that formats the entire run history into a list suitable for being the input to the nextRunner.run()
call, which is how you build conversational memory.
Let’s inspect it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#
# Inspecting the RunResult object
#
# ... (agent and tool definitions from before) ...
def inspect_result():
# ...
financial_agent = Agent(
# ... same definition
)
result = Runner.run_sync(
financial_agent,
"What is the current price of Google's stock?"
)
print("--- Final Output ---")
print(result.final_output)
print("
--- Last Agent ---")
print(f"The last agent to run was: {result.last_agent.name}")
print("
--- New Items (Structured History) ---")
for item in result.new_items:
print(f" - Type: {item.type}, Agent: {item.agent.name}")
if item.type == 'tool_call_output_item':
print(f" Output: {item.output}")
# Expected Output:
#
# --- Final Output ---
# The current price for GOOGL is $...
#
# --- Last Agent ---
# The last agent to run was: Financial Assistant
#
# --- New Items (Structured History) ---
# - Type: tool_call_item, Agent: Financial Assistant
# - Type: tool_call_output_item, Agent: Financial Assistant
# Output: ...
# - Type: message_output_item, Agent: Financial Assistant
The
result.to_input_list()
method is the key to creating conversational agents. By taking the result of one run and using it as the basis for the input to the next, you build up a conversation history that the agent can refer to.
1 2 3 4 5 6 7 8 # Turn 1 result1 = Runner.run_sync(agent, "My name is Alex.") # Turn 2 next_turn_input = result1.to_input_list() next_turn_input.append({"role": "user", "content": "What is my name?"}) result2 = Runner.run_sync(agent, next_turn_input) # The agent will correctly answer "Your name is Alex."
Chapter Summary
In this chapter, we took our first practical steps with the OpenAI Agents SDK. We set up our environment, defined a simple agent, and used the Runner
to execute it. We then empowered our agent with a custom Python tool, which unveiled the multi-turn Agent Loop that the SDK manages on our behalf. We’ve seen how the agent can reason, select a tool, and use its output to generate a final answer. Finally, we briefly looked at asynchronous execution, streaming, and the detailed RunResult
object.
You now have a solid mental model of the fundamental execution flow. In the next chapter, we’ll explore the ecosystem beyond OpenAI, showing you how to integrate over 100 different models from various providers into your agentic workflows.