0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
27 small wins to finish your pathNext lesson →

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

PieceFrom lesson
lookup_order toolThe desk agent and its tools
Refund specialist handoffA refund specialist by handoff
no_passwords input guardrailGuarding the desk's input and output
SQLiteSession per customerThe desk that remembers with a session

The tool and the guardrail

The tool answers order questions; the guardrail blocks any message with a password.

python
@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.

python
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).

python
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.

python
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.

Example
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.name is 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

RunHow it ends
Order or refund messagefinal_output is produced, by the desk or the specialist
Message with a passwordInputGuardrailTripwireTriggered 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.
Watch out. Give each customer their own session id, or two people share one memory. The guardrail runs on the first agent only, so keep it on the desk, not the specialist.
Try it yourself
  • 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.