Example: OpenAI Agents SDK — customer support
Use case: A multi-agent customer support system using the OpenAI Agents SDK’s native handoff feature — a triage agent routes tickets to billing, technical, or account specialists. Wrapped with Agentspan in one line.
What Agentspan adds to the OpenAI Agents SDK
The OpenAI Agents SDK handles your agent definitions, handoffs, and tool routing. Agentspan adds a production execution layer without changing any of that:
- Crash recovery — if your process dies during a multi-step resolution, Agentspan resumes when a worker reconnects
- Full handoff trace — every handoff between agents is a logged step, visible in the UI at
http://localhost:6767 - Human approval on tools — add
approval_required=Trueto any tool to pause execution for human sign-off - Execution history — every ticket run is stored with inputs, outputs, and timing
Your agent definitions, handoff configurations, and tool implementations stay exactly as written.
Before: plain OpenAI Agents SDK
Standard code using the OpenAI Agents SDK. The native handoff pattern works well, but runs have no history, no crash recovery, and no human-in-the-loop.
from agents import Agent, Runner, function_tool, handoff
# ── Tools ────────────────────────────────────────────────────────────────────
@function_tool
def get_account(customer_id: str) -> dict:
"""Look up a customer's account: plan, billing status, usage."""
return accounts_db.find({"id": customer_id})
@function_tool
def get_invoice(invoice_id: str) -> dict:
"""Fetch an invoice by ID."""
return billing_api.get_invoice(invoice_id)
@function_tool
def process_refund(invoice_id: str, reason: str) -> dict:
"""Issue a full refund for an invoice."""
return billing_api.refund(invoice_id, reason=reason)
@function_tool
def get_ticket_history(customer_id: str) -> list[dict]:
"""Get the last 5 support tickets for a customer."""
return tickets_db.find({"customer_id": customer_id}).limit(5)
@function_tool
def reset_password(customer_id: str) -> dict:
"""Send a password reset email to the customer."""
return auth_api.send_reset(customer_id)
@function_tool
def check_service_status(region: str) -> dict:
"""Check current service health for a region."""
return status_api.get(region)
@function_tool
def escalate_to_human(ticket_id: str, reason: str, priority: str) -> dict:
"""Escalate this ticket to a human agent."""
return queue_api.escalate(ticket_id, reason=reason, priority=priority)
# ── Specialist agents ─────────────────────────────────────────────────────────
billing_agent = Agent(
name="billing_specialist",
model="gpt-4o",
instructions="""You handle billing questions: invoices, charges, refunds, plan changes.
Always look up the account first. Process refunds only for clear billing errors.
For amounts over $200, escalate to a human.""",
tools=[get_account, get_invoice, process_refund, escalate_to_human],
)
technical_agent = Agent(
name="technical_specialist",
model="gpt-4o",
instructions="""You handle technical issues: login problems, service outages, API errors.
Always check service status first. If the issue is account-specific, look up the account.
Reset passwords only after verifying the customer's identity.""",
tools=[get_account, check_service_status, reset_password, escalate_to_human],
)
account_agent = Agent(
name="account_specialist",
model="gpt-4o",
instructions="""You handle account changes: upgrades, downgrades, cancellations, data exports.
Always look up the account and ticket history before making changes.
For cancellations, attempt retention first.""",
tools=[get_account, get_ticket_history, escalate_to_human],
)
# ── Triage agent with handoffs ────────────────────────────────────────────────
triage_agent = Agent(
name="support_triage",
model="gpt-4o-mini", # fast, cheap — just routes
instructions="""You are a support triage agent. Understand the customer's issue
and hand off to the right specialist immediately.
- Billing, charges, invoices, refunds → billing_specialist
- Login, outages, API errors, technical issues → technical_specialist
- Plan changes, cancellations, account settings → account_specialist""",
handoffs=[
handoff(billing_agent),
handoff(technical_agent),
handoff(account_agent),
],
)
# ── Run (plain OpenAI Agents SDK — no durability) ─────────────────────────────
result = Runner.run_sync(
triage_agent,
"Hi, I was charged $99 twice last month (invoice INV-8821). Can I get a refund?",
)
print(result.final_output)
After: wrapped with Agentspan
Pass the OpenAI Agents SDK agent directly to runtime.run(). Agentspan auto-detects it — no integration module needed.
from agentspan.agents import AgentRuntime
# was: result = Runner.run_sync(triage_agent, message)
with AgentRuntime() as runtime:
result = runtime.run(triage_agent, message)
print(result.output)
print(f"Run ID: {result.execution_id}")
What you gain
Full handoff trace — every handoff between agents is a logged step. Open http://localhost:6767 to see exactly which specialist handled the ticket and what they did.
Crash recovery — if your process dies during a complex multi-step billing resolution, Agentspan resumes when a worker reconnects. The customer’s ticket isn’t dropped.
Run history — every execution is stored with inputs, outputs, token usage, and timing.
Async variant
from agentspan.agents import run_async
async def handle_ticket(message: str):
result = await run_async(triage_agent, message)
return result.output
asyncio.run(handle_ticket("I was charged twice last month"))
Fire-and-forget for slow tickets
from agentspan.agents import start
# Returns immediately
handle = start(triage_agent, customer_message)
print(f"Ticket queued: {handle.execution_id}")
# Get result later
result = handle.stream().get_result()
print(result.output)
Stream events as they happen
from agentspan.agents import stream
for event in stream(triage_agent, customer_message):
if event.type == "handoff":
print(f" → routed to {event.target}")
elif event.type == "tool_call":
print(f" → {event.tool_name}({event.args})")
elif event.type == "done":
print(f"\n{event.output}")
Adding human approval for large refunds
Use Agentspan’s @tool decorator with approval_required=True on any sensitive tool:
from agentspan.agents import tool as agentspan_tool
@agentspan_tool(approval_required=True)
def process_refund(invoice_id: str, reason: str) -> dict:
"""Issue a full refund. Requires human approval."""
return billing_api.refund(invoice_id, reason=reason)
# Use it in your OpenAI Agents SDK agent as normal
billing_agent = Agent(
name="billing_specialist",
tools=[get_account, get_invoice, process_refund, escalate_to_human],
...
)
Testing
from agentspan.agents.testing import mock_run, MockEvent, expect
result = mock_run(
triage_agent,
"I was charged twice last month",
events=[
MockEvent.handoff("billing_specialist"),
MockEvent.done("I've looked into your account. A refund has been initiated."),
]
)
expect(result).completed()