A full run, and when it refuses
The full desk is one Agent with a tool, a handoff, a guardrail and a session, and this run shows it answering, remembering, routing, and refusing.
Last updated: 28 Sep, 2026 · openai-agents 0.22.3
Each piece was added on its own across this part. Here they sit on one desk at once, and one program runs four scenarios: a normal answer, a remembered follow-up, a refund routed to the specialist, and a blocked request, the failure mode that proves the guard works.
The pieces, and where each came from
| Piece | From lesson |
|---|---|
| lookup_order tool | The desk agent and its tools |
| Refund specialist handoff | A refund specialist by handoff |
| no_passwords input guardrail | Guarding the desk's input and output |
| SQLiteSession per customer | The desk that remembers with a session |
The tool and the guardrail
The tool answers order questions; the guardrail blocks any message with a password.
@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."
@input_guardrail
async def no_passwords(ctx, agent, user_input) -> GuardrailFunctionOutput:
text = user_input if isinstance(user_input, str) else str(user_input)
tripped = "password" in text.lower()
return GuardrailFunctionOutput(output_info={}, tripwire_triggered=tripped)The specialist and the assembled desk
One stand-in, DeskModel, serves both agents. It combines the behaviours from earlier in this part: it looks up a named order, answers a follow-up from the remembered id, hands a refund to the specialist, and lets that specialist compose the refund reply. The desk carries all four capabilities: the tool, the handoff, the guardrail, and, per run, a session.
refund = Agent(name="Refund specialist",
instructions="Handle refund requests.", model=DeskModel())
desk = Agent(name="Shop desk",
instructions="Help shoppers; send refunds to the specialist.",
tools=[lookup_order], handoffs=[refund],
input_guardrails=[no_passwords], model=DeskModel())The desk model, from lookup to memory to handoff
After handling a fresh tool result, the new-turn branch decides what to do. It reads the order id from the whole history, routes a refund to the specialist, looks up a named order, or answers a follow-up from memory when the message mentions shipping (words like ship, yet, where).
latest = latest_user_text(input).lower()
found = re.search(r"\b[A-Z]\d+\b", all_user_text(input)) # id from the whole history
order_id = found.group(0) if found else None
if handoffs and "refund" in latest: # route refunds away
return ModelResponse(output=[_tool_call(handoffs[0].tool_name, "{}")],
usage=Usage(), response_id=None)
if tools and order_id and "order" in latest: # look up a named order
return ModelResponse(output=[_tool_call("lookup_order",
'{"order_id": "%s"}' % order_id)], usage=Usage(), response_id=None)
if order_id and any(w in latest for w in ("ship", "yet", "where")): # from memory
return ModelResponse(output=[_message(
"Your order %s shipped on 3 March and arrives 7 March." % order_id)],
usage=Usage(), response_id=None)Four scenarios in one run
Alice asks about an order over two turns; Bob asks for a refund; Carol's message carries a password.
alice = SQLiteSession("alice")
print("Order turn 1:", Runner.run_sync(desk, "Where is my order A17?", session=alice).final_output)
print("Order turn 2:", Runner.run_sync(desk, "Has it shipped yet?", session=alice).final_output)
routed = Runner.run_sync(desk, "I want a refund", session=SQLiteSession("bob"))
print("Refund handled by:", routed.last_agent.name)
try:
Runner.run_sync(desk, "My password is hunter2", session=SQLiteSession("carol"))
except InputGuardrailTripwireTriggered:
print("Refused: input guardrail tripped, the desk never answered")The whole desk in one run
The assembled desk, with each customer on their own session, run end to end.
from agents import (Agent, Runner, function_tool, set_tracing_disabled,
input_guardrail, GuardrailFunctionOutput, InputGuardrailTripwireTriggered,
RunContextWrapper, SQLiteSession)
from agents.models.interface import Model
from agents.items import ModelResponse
from agents.usage import Usage
from openai.types.responses import (
ResponseOutputMessage, ResponseOutputText, ResponseFunctionToolCall,
)
import re
set_tracing_disabled(True)
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):
return ResponseFunctionToolCall(
id="fc", call_id="call_1", name=name, arguments=arguments, type="function_call",
)
def all_user_text(input):
if isinstance(input, str):
return input
parts = []
for it in input:
d = it if isinstance(it, dict) else it.__dict__
if d.get("role") == "user":
c = d.get("content")
if isinstance(c, str):
parts.append(c)
elif isinstance(c, list):
parts.append(" ".join(
(p if isinstance(p, dict) else p.__dict__).get("text", "") for p in c))
return " ".join(parts)
def latest_user_text(input):
if isinstance(input, str):
return input
for it in reversed(input):
d = it if isinstance(it, dict) else it.__dict__
if d.get("role") == "user":
c = d.get("content")
if isinstance(c, str):
return c
if isinstance(c, list):
return " ".join(
(p if isinstance(p, dict) else p.__dict__).get("text", "") for p in c)
return ""
def newest_item(input):
if isinstance(input, str) or not input:
return {}
last = input[-1]
return last if isinstance(last, dict) else last.__dict__
class DeskModel(Model):
async def get_response(self, system_instructions, input, model_settings, tools,
output_schema, handoffs, tracing, **k):
newest = newest_item(input)
if newest.get("type") == "function_call_output":
out = newest.get("output") or ""
# A handoff transfer looks like {"assistant": ...}; the specialist answers for real.
if out.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(out)], usage=Usage(), response_id=None)
latest = latest_user_text(input).lower()
found = re.search(r"\b[A-Z]\d+\b", all_user_text(input))
order_id = found.group(0) if found else None
if handoffs and "refund" in latest:
return ModelResponse(output=[_tool_call(handoffs[0].tool_name, "{}")],
usage=Usage(), response_id=None)
if tools and order_id and "order" in latest:
return ModelResponse(output=[_tool_call("lookup_order",
'{"order_id": "%s"}' % order_id)], usage=Usage(), response_id=None)
if order_id and any(w in latest for w in ("ship", "arriv", "deliver", "status", "yet", "where")):
return ModelResponse(output=[_message(
"Your order %s shipped on 3 March and arrives 7 March." % order_id)],
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, *a, **k):
raise NotImplementedError
@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."
@input_guardrail
async def no_passwords(ctx: RunContextWrapper, agent: Agent, user_input) -> GuardrailFunctionOutput:
text = user_input if isinstance(user_input, str) else str(user_input)
tripped = "password" in text.lower()
return GuardrailFunctionOutput(output_info={"blocked": tripped}, tripwire_triggered=tripped)
refund = Agent(name="Refund specialist", instructions="Handle refund requests.", model=DeskModel())
desk = Agent(
name="Shop desk",
instructions="Help shoppers; send refunds to the specialist.",
tools=[lookup_order],
handoffs=[refund],
input_guardrails=[no_passwords],
model=DeskModel(),
)
# A normal order question, remembered across two turns on one session
alice = SQLiteSession("alice")
print("Order turn 1:", Runner.run_sync(desk, "Where is my order A17?", session=alice).final_output)
print("Order turn 2:", Runner.run_sync(desk, "Has it shipped yet?", session=alice).final_output)
# A refund routes to the specialist, who composes the refund reply
bob = SQLiteSession("bob")
routed = Runner.run_sync(desk, "I want a refund", session=bob)
print("Refund handled by:", routed.last_agent.name)
# The failure mode: a password in the message is refused before the model runs
try:
Runner.run_sync(desk, "My password is hunter2", session=SQLiteSession("carol"))
except InputGuardrailTripwireTriggered:
print("Refused: input guardrail tripped, the desk never answered")What each scenario showed
- Order turn 1 called the tool and answered with the order line.
- Order turn 2 named no id, yet answered about A17 because the model read it from Alice's replayed first turn, not from an echo of the last tool result.
- The refund routed off the desk:
last_agent.nameis Refund specialist. - The password tripped the guardrail, so the run raised before the model ran, the desk refusing rather than answering.
A clean run vs a tripped guardrail
| Run | How it ends |
|---|---|
| Order or refund message | final_output is produced, by the desk or the specialist |
| Message with a password | InputGuardrailTripwireTriggered is raised, no answer is produced |
When to build a desk like this
- A support line that looks things up, routes the hard cases, remembers the customer, and refuses unsafe input.
- Any assistant where a wrong or unsafe answer costs more than a refusal.
Related
- Previous: The desk that remembers with a session
- Next: Swapping in a real model
- Reference: Running agents
- Give Bob Alice's session id, then send Bob a shipping question and watch Alice's order A17 answer leak in.
- Add the word password to the refund message and see it blocked before the handoff.
- Add a third order turn on Alice's session and confirm the memory still holds.
This is what real progress feels like.