The desk agent and its tools
The shop desk is an Agent that answers an order question by calling a tool, the first piece of the support desk you finish in this part.
Last updated: 28 Sep, 2026 · openai-agents 0.22.3
Runner and the RunResult object arrived in the lesson on Runner and the RunResult object, and the tool decorator arrived in function_tool: a function the model can call. Here they meet: one agent, one tool, one order question, run against the ShopModel stand-in so no API key is needed.
The pieces, and where each came from
| Piece | From lesson |
|---|---|
| An Agent with a name and instructions | Agent: name, instructions and tools |
| A @function_tool the model can call | function_tool: a function the model can call |
| Runner.run_sync and result.final_output | Runner and the RunResult object |
| ShopModel, a model that runs with no key | Model: a stand-in you can run without a key |
The lookup tool
A tool is a plain function with the @function_tool decorator. Its docstring tells the model what it does.
@function_tool
def lookup_order(order_id: str) -> str:
"Look up an order by its id."
return f"Order {order_id}: shipped on 3 March, arriving 7 March."The desk agent
The desk is an Agent that carries the tool in its tools list and uses the stand-in model.
desk = Agent(
name="Shop desk",
instructions="Help shoppers with their orders.",
tools=[lookup_order], # the desk may call this tool
model=ShopModel(), # the stand-in, no API key
)Asking one order question
Runner.run_sync drives the run and hands back a result; final_output is the answer.
result = Runner.run_sync(desk, "Where is my order A17?")
print(result.final_output)View 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)
The desk answering an order question
The whole desk in one file, ready to run against the stand-in.
from agents import Agent, Runner, function_tool, set_tracing_disabled
from shop_model import ShopModel
set_tracing_disabled(True)
@function_tool
def lookup_order(order_id: str) -> str:
"Look up an order by its id."
return f"Order {order_id}: shipped on 3 March, arriving 7 March."
desk = Agent(
name="Shop desk",
instructions="Help shoppers with their orders.",
tools=[lookup_order],
model=ShopModel(),
)
result = Runner.run_sync(desk, "Where is my order A17?")
print(result.final_output)
Order A17: shipped on 3 March, arriving 7 March.
What the run produced
- The stand-in saw the word order, so it asked to call
lookup_orderwith id A17 instead of answering straight away. - The runner ran the tool and fed its return value back to the model.
- The final answer is the tool's line, because the model turns a tool result into the reply.
A desk with a tool vs a desk with none
| Setup | The desk's reply to the order question |
|---|---|
| Desk with lookup_order | Order A17: shipped on 3 March, arriving 7 March. |
| Desk with no tools | How can I help with your order? |
When the desk needs a tool
- Any answer that depends on live data, an order status, a balance, a booking, belongs in a tool, not in the instructions.
- The desk stays small: the model decides, the tool fetches, the model replies.
Related
- Previous: Capping a run with max_turns
- Next: A refund specialist by handoff
- Reference: Tools
- Ask about a different order id and notice the stand-in still returns A17; a real model would read the id from your message.
- Remove
tools=[lookup_order]and rerun; the desk falls back to the greeting. - Add a second tool,
store_hours(), and see that the stand-in still reaches for lookup_order on an order question.
Every expert started right here.