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:
- Pass it explicitly via the existing
Runner.run(..., hooks=)parameter. - Auto-inject with
instrument_openai_agents()(Python), which attachesAgentPingHooksto anyRunner.runcall that did not passhooks=.
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
- Python SDK:
agentping.AgentPingHooks+agentping.instrument_openai_agents()in agentping-sdk-python - TypeScript SDK:
AgentPingHooksin agentping-sdk-typescript