Automating Dispute Management with Agents SDK and Stripe API (part 2)

July 21, 2025
tl;dr: I explored the OpenAI Agents SDK with different triage agent setups. With extra tools and handoffs, the workflow finally closed disputes itself. Figuring out the handoff naming in the SDK source code was fun.

You're reading part 2 of my exploration of the OpenAI Agents SDK, inspired by Dan Bell's post: Automating Dispute Management with Agents SDK and Stripe API:

  • Check out Part 1 where I explored the Stripe API,

  • Check out Part 3 where I looked at the JSON data of the requests and responses that the OpenAI Agents SDK sends to the Responses API,

  • Check out Part 4 where I decided to try removing all the agents from the original code.

Once I was clear on how to call the Stripe API, I kept diging into the post Dan Bell's post.

Next step: get to know the OpenAI Agents SDK.

By default, OpenAI Agents SDK uses the OpenAI Responses API. Tracing, which is enabled by default, relies on the Traces API (but can also be set to other tracing processors). This setup is convenient since we can monitor our agents straight from the Traces dashboard.

OpenAI Traces dashboard monitoring AI agent workflows in automation

Testing with different Triage agents

To observe behavior when running the agent workflow:

await Runner.run(triage_agent, input=...)

I ran the workflow four times, each with a different triage_agent configuration:

  1. No tools, no handoffs:

    triage_agent = Agent(
        name="Triage Agent No Tools",
        instructions=(
            "Please do the following:\n"
            "1. Find the order ID from the payment intent's metadata.\n"
            "2. Retrieve detailed information about the order (e.g., shipping status).\n"
            "3. If the order has shipped, escalate this dispute to the investigator agent.\n"
            "4. If the order has not shipped, accept the dispute.\n"
        ),
        model="gpt-4o"
    )
  2. One tool, no handoffs:

    triage_agent = Agent(
        name="Triage Agent One Tool",
        instructions=(...),
        model="gpt-4o",
        tools=[retrieve_payment_intent]
    )
  3. Two tools, no handoffs:

    triage_agent = Agent(
        name="Triage Agent With Tools",
        instructions=(...),
        model="gpt-4o",
        tools=[retrieve_payment_intent, get_order]
    )
  4. Two tools, two handoffs (as in the article):

    triage_agent = Agent(
        name="Triage Agent With Tools And Agent",
        instructions=(...),
        model="gpt-4o",
        tools=[retrieve_payment_intent, get_order],
        handoffs=[accept_dispute_agent, investigator_agent]
    )

I used the same PaymentIntent each time, created like this:

payment = stripe.PaymentIntent.create(
  amount=2000,
  currency="usd",
  payment_method = "pm_card_createDisputeProductNotReceived",
  confirm=True,
  metadata={"order_id": "1234"},
  off_session=True,
  automatic_payment_methods={"enabled": True},
)

And ran the workflow:

import asyncio
asyncio.run(process_dispute(payment.id, triage_agent))

Let's break down what happened:

With no tool provided, the workflow stopped with a text output only. No tool calls were made, and no action was taken on the dispute:

To address the dispute, we need to follow these steps:

1. **Find the Order ID from the Payment Intent's Metadata:**
   - Access the payment intent with ID `pi_3RnJp1QuMzbiv2kf14SQqrm8`.
   - Extract the metadata to locate the associated order ID.

2. **Retrieve Detailed Information About the Order:**
   - Use the order ID to fetch order details such as shipping status.

3. **Check the Order's Shipping Status:**
   - If the order has shipped:
     - Escalate the dispute to the investigator agent.
   - If the order has not shipped:
     - Accept the dispute.

Proceed with the above steps to determine the next course of action.

OpenAI Traces UI: displaying AI agent workflow and span details for Responses API

With one tool, after the first call to gpt-4o, the agent invoked retrieve_payment_intent. But since no tool for order info was available, the agent stopped, asking what method to use next:

The order ID from the payment intent's metadata is "1234".

Next, I'll retrieve detailed information about the order, such as its
shipping status. Could you please provide the service or method you'd
like me to use to obtain the order details?

Analysis of AI agents and function tool calls in OpenAI Traces for workflow automation

With both tools, the agent first called retrieve_payment_intent to get the order ID, then called get_order for fulfillment details. It then suggested accepting the dispute. The workflow didn't actually close the dispute, as no handoff was defined. Here's the final output:

The order with ID **1234** has not been shipped. You should accept the
dispute.

OpenAI Traces: visual report of Responses API, function tool use, and agent actions

Things got interesting when I added both tools and handoffs to the triage agent (as in the article). This time, the agent called both tools, then the LLM decided to accept the dispute, and invoked a function call to transfer_to_accept_dispute_agent tool. The OpenAI Agents SDK treated this as a handoff. The new agent, accept_dispute_agent, took over, with access to the full conversation history (by default). It then called the close_dispute tool, closing the dispute in Stripe using stripe.Dispute.close(...). Here's the final output:

Dispute Details:
- Dispute ID: dp_1RnJp2QuMzbiv2kfeTMYrELd
- Amount: $20.00
- Reason for Dispute: Product not received

Order Details:
- Fulfillment status of the order: Not shipped

Reasoning for closing the dispute:
The dispute was accepted and closed because the order associated with
the payment was not shipped. The customer was charged for a product
that they did not receive, justifying the acceptance of the dispute.

Visualization of AI agents, handoffs, and tool calls in workflow automation with OpenAI Traces

Who adds the transfer_to_ prefix to handoffs, Responses API or OpenAI Agents SDK?

Sometimes, you know, sometimes you don't! I didn't and it was not clear to me.

tl;dr: The OpenAI Agents SDK is responsible for adding this prefix.

In the Traces dashboard, I noticed that the LLM could use four functions: retrieve_payment_intent, get_order, transfer_to_accept_dispute_agent and transfer_to_dispute_intake_agent.

Detailed OpenAI Traces UI showing AI agent workflow and span analytics for automation

Here's what I wondered: when we define triage_agent like this

triage_agent = Agent(
    name="...",
    instructions="...",
    model="gpt-4o",
    tools=[retrieve_payment_intent, get_order],
    handoffs=[accept_dispute_agent, investigator_agent]
)

why does the Responses API offer tool names like get_order, but for handoffs, they become transfer_to_dispute_agent and transfer_to_dispute_intake_agent (instead of, say, transfer_to_investigator_agent)?

Is the Responses API changing the tool name, or is it coming from the request payload?

Nothing in the Response API reference! So this must be the SDK's responsibility.

The Handoffs documentation quickly clears this up:

Handoffs are represented as tools to the LLM. So if there's a handoff to an agent named "Refund Agent", the tool would be called transfer_to_refund_agent.

And the reason for investigator_agent showing as transfer_to_dispute_intake_agent is that its name is "Dispute Intake Agent".

As I never trust the docs blindly, I checked the OpenAI Agents SDK source. Searching for transfer_to, default_tool_name, and handoff\(, I confirmed that when the workflow runs, it does exactly what the docs say:

# src/agents/handoffs.py
@dataclass
class Handoff(Generic[TContext, TAgent]):
    # ...
    @classmethod
    def default_tool_name(cls, agent: AgentBase[Any]) -> str:
        return _transforms.transform_string_function_style(f"transfer_to_{agent.name}")
    # ...

# ...

def handoff(agent: Agent[TContext], ...) -> Handoff[TContext, Agent[TContext]]:
    # ...
    tool_name = tool_name_override or Handoff.default_tool_name(agent)
    # ...
# src/agents/util/_transforms.py
def transform_string_function_style(name: str) -> str:
    # Replace spaces with underscores
    name = name.replace(" ", "_")

    # Replace non-alphanumeric characters with underscores
    name = re.sub(r"[^a-zA-Z0-9]", "_", name)

    return name.lower()

That's all I have for today! Talk to you soon ;)

References

Built with one.el.