Runner and the RunResult object
Runner is the class that runs an agent, and RunResult is the object it returns, holding the final answer and every step the run took.
Last updated: 28 Sep, 2026 · openai-agents 0.22.3
The stand-in model lesson built a model; the Runner is what drives it. You hand the Runner an agent and a message, it runs the agent loop, and it gives back a RunResult. That result carries the final answer and a list of each step, which is your window into what happened.
The Runner methods
There are three ways to start a run. run_sync blocks until the run finishes; run is the async version; run_streamed returns events as they happen.
from agents import Runner
result = Runner.run_sync(agent, "hello") # blocks until done
# await Runner.run(agent, "hello") -> async version
# Runner.run_streamed(agent, "hello") -> events as they happen
print(result.final_output) # the last message textChoosing run_sync, run or run_streamed
Use run_sync in a plain script. Inside async code, await Runner.run(...). Use run_streamed when you want to watch the reply arrive; the streaming lesson covers it.
result = Runner.run_sync(agent, "hello") # in a normal scriptReading final_output
final_output is the text of the last message the model produced. For a plain reply, that is the whole answer.
print(result.final_output) # -> the model's final message textListing the steps with new_items
new_items is the list of items the run created, in order: tool calls, tool outputs and messages. It is the inspection tool you use for the rest of the course.
print(len(result.new_items)) # how many steps
for item in result.new_items:
print(type(item).__name__) # the kind of each stepView the code here
"""A deterministic stand-in Model for the OpenAI Agents SDK course.
It implements the Model interface so an Agent runs with no API key. It reads the
last user message and the tool results out of `input`, and returns either a tool
call, a handoff, or a final message. Swap it for a real model at the end.
"""
from agents.models.interface import Model
from agents.items import ModelResponse
from agents.usage import Usage
from openai.types.responses import (
ResponseOutputMessage, ResponseOutputText, ResponseFunctionToolCall,
ResponseCompletedEvent, ResponseTextDeltaEvent, Response,
)
def _message(text):
return ResponseOutputMessage(
id="msg", role="assistant", type="message", status="completed",
content=[ResponseOutputText(text=text, type="output_text", annotations=[])],
)
def _tool_call(name, arguments, call_id="call_1"):
return ResponseFunctionToolCall(
id="fc", call_id=call_id, name=name, arguments=arguments, type="function_call",
)
def last_user_text(input):
if isinstance(input, str):
return input
for item in reversed(input):
d = item if isinstance(item, dict) else item.__dict__
if d.get("role") == "user":
content = d.get("content")
if isinstance(content, str):
return content
if isinstance(content, list):
for part in content:
pd = part if isinstance(part, dict) else part.__dict__
if pd.get("text"):
return pd["text"]
return ""
def tool_output(input):
if isinstance(input, str):
return None
for item in reversed(input):
d = item if isinstance(item, dict) else item.__dict__
if d.get("type") == "function_call_output":
return d.get("output")
return None
class ShopModel(Model):
async def get_response(self, system_instructions, input, model_settings, tools,
output_schema, handoffs, tracing, *, previous_response_id=None,
conversation_id=None, prompt=None):
result = tool_output(input)
if result is not None:
# A handoff transfer looks like {"assistant": "..."}; the specialist answers for real.
if result.strip().startswith('{"assistant"'):
if "refund" in (system_instructions or "").lower():
return ModelResponse(
output=[_message(
"Your refund is approved and will be processed in 5 to 7 days.")],
usage=Usage(), response_id=None)
return ModelResponse(output=[_message("Handled by the specialist.")],
usage=Usage(), response_id=None)
return ModelResponse(output=[_message(result)], usage=Usage(), response_id=None)
text = last_user_text(input).lower()
if handoffs and "refund" in text:
return ModelResponse(output=[_tool_call(handoffs[0].tool_name, "{}")],
usage=Usage(), response_id=None)
if tools and "order" in text:
return ModelResponse(output=[_tool_call("lookup_order", '{"order_id": "A17"}')],
usage=Usage(), response_id=None)
return ModelResponse(output=[_message("How can I help with your order?")],
usage=Usage(), response_id=None)
async def stream_response(self, system_instructions, input, model_settings, tools,
output_schema, handoffs, tracing, *, previous_response_id=None,
conversation_id=None, prompt=None):
text = "How can I help with your order?"
for i, word in enumerate(text.split()):
yield ResponseTextDeltaEvent(
type="response.output_text.delta", delta=word + " ",
content_index=0, item_id="msg", output_index=0,
sequence_number=i, logprobs=[],
)
response = Response(
id="r", created_at=0.0, model="shop-standin", object="response",
output=[_message(text)], parallel_tool_calls=False,
tool_choice="auto", tools=[],
)
yield ResponseCompletedEvent(type="response.completed", response=response,
sequence_number=99)
Running the agent and counting its steps
This imports ShopModel from the shop_model.py built in the model lesson, runs one message, and prints the answer and the steps. A plain greeting produces a single step.
from agents import Agent, Runner, set_tracing_disabled
from shop_model import ShopModel
set_tracing_disabled(True)
agent = Agent(name="Shop", instructions="Help with orders.", model=ShopModel())
result = Runner.run_sync(agent, "hello")
print("final_output:", result.final_output)
print("new_items:", len(result.new_items))
for item in result.new_items:
print("-", type(item).__name__)final_output: How can I help with your order? new_items: 1 - MessageOutputItem
What final_output and new_items showed
- final_output is the greeting, the text of the last message.
- new_items had one entry, because the model replied once and called no tool.
- MessageOutputItem is the item type for a message; tool runs add other types, seen in the agent-loop lesson.
run_sync vs run vs run_streamed
| run_sync | run | run_streamed | |
|---|---|---|---|
| Call style | Blocking | await | await, then read events |
| Returns | RunResult | RunResult | RunResultStreaming |
| Use in | Plain scripts | Async code | Live output |
Where new_items helps
- Confirming a tool ran, not only that an answer came back.
- Debugging a run by reading each step in order.
- Counting how many turns a run took before it settled.
run_sync starts its own event loop, so calling it inside code that already runs one (an async function, some notebooks) raises a runtime error. Use await Runner.run(agent, message) there instead.Related
- Previous: Model: a stand-in you can run without a key
- Next: function_tool: a function the model can call
- See also: Running agents
- Reference: Results
- Print
result.final_outputon its own and read the type. - Loop over
result.new_itemsand print each item'stype().__name__. - Send a longer message and check
new_itemsstill has one entry.
This is what real progress feels like.