OpenAI Agents SDK

AgentPingHooks is a RunHooks implementation that emits llm_call on every model turn, tool_call on every tool invocation, and handoff on every agent-to-agent transition. Hooks fire regardless of which model each agent uses, so multi-provider agent loops are covered without wrapping any client.

Two ways to attach it:

  1. Pass it explicitly via the existing Runner.run(..., hooks=) parameter.
  2. Auto-inject with instrument_openai_agents() (Python), which attaches AgentPingHooks to any Runner.run call that did not pass hooks=.

Install

Python:

pip install "agentping-sdk[openai-agents]"

TypeScript:

npm install @agentping/sdk @openai/agents

Python supports openai-agents >= 0.1.0.

Quickstart

Python, passing hooks explicitly:

import agentping
from agents import Agent, Runner

agentping.init()

triage_agent = Agent(
    name="Triage",
    instructions="Route customer questions to the right specialist agent.",
    model="gpt-4o-mini",
    handoffs=[billing_agent, support_agent],
)

with agentping.run("triage") as r:
    result = await Runner.run(
        triage_agent,
        "I was overcharged on invoice 4821",
        hooks=agentping.AgentPingHooks(),
    )
    r.score("confidence", 0.92)

AgentPingHooks() picks up the active run from with agentping.run(...). To skip passing hooks= on every call, auto-inject once at module load:

agentping.init()
agentping.instrument_openai_agents()

# AgentPingHooks is auto-attached when you don't pass hooks=.
with agentping.run("triage") as r:
    result = await Runner.run(triage_agent, "billing question")

The patch is idempotent and detects user-supplied hooks; passing hooks= explicitly still overrides it.

TypeScript passes the hooks via Runner.run's hooks option (constructor run argument is optional, resolved from runScope when omitted):

import * as agentping from "@agentping/sdk";
import { Agent, Runner } from "@openai/agents";

agentping.init({ apiKey: process.env.AGENTPING_API_KEY });

const run = agentping.run("triage");
const result = await Runner.run(
  triageAgent,
  "billing question",
  { hooks: new agentping.AgentPingHooks(run) },
);

await run.finish({ status: "success" });

What's captured

Hook AgentPing event Data
on_llm_end llm_call provider, model, input/output tokens, latency
on_tool_start tool_call tool name
on_handoff handoff from agent, to agent

A triage agent that routes to billing and escalates to a manager produces a clean trace:

run triage
├── llm_call (Triage, 412 in / 28 out)
├── handoff (Triage → Billing)
├── llm_call (Billing, 891 in / 102 out)
├── handoff (Billing → Manager)
├── llm_call (Manager, 1240 in / 187 out)
└── score confidence 0.92

Advanced (optional)

Subclass AgentPingHooks to add behavior on top of the defaults. Call super() to keep the automatic events, then add your own:

class MyHooks(agentping.AgentPingHooks):
    async def on_tool_start(self, context, agent, tool):
        await super().on_tool_start(context, agent, tool)
        # add your own logic here

For very large agent loops (30+ LLM calls), override on_tool_start to a no-op; on_llm_end alone captures all the cost. Guardrail tripwires raise inside the agent loop, so the run's context manager already records status="failed" with no extra wiring.

Source