Why Agents SDK needs a before tool call hook

For now, I’d probably avoid making check_spend a model-callable tool.

I’d wrap each expensive tool with a normal code-level budget check, or attach a tool input guardrail if you’re using the newer SDK guardrail features.

The key part is that the model should not be responsible for enforcing the budget.

Also, I’d keep a hard limit outside the agent loop entirely.

Even if the agent gets stuck, the runner/backend should be able to stop after X tool calls, X tokens, or X dollars.

Agree completely — the model shouldn’t own budget enforcement, that’s the core issue with the tool-based approach I showed.

The guardrail path is cleaner. Something like:

from agents import Agent, Runner, input_guardrail, GuardrailFunctionOutput

@input_guardrail

async def spend_guard(ctx, agent, input):

approved = check_budget(estimated_cost=0.05) # your policy check

return GuardrailFunctionOutput(

output_info={"approved": approved},

tripwire_triggered=not approved,

)

agent = Agent(

name="Research Agent",

input_guardrails=[spend_guard],

tools=[...],

)

The hard outer limit is the right safety net — but guardrails give you per-call granularity before the runner even sees it.

Is there a way to pass per-tool cost estimates into guardrails yet, or does it only have access to the full input at that point?

The check_spend tool breaking under loops isn’t really tool-vs-hook — it’s that anything the agent can call sits inside the boundary it’s supposed to guard, so under a retry loop the guard is just one more thing the loop spins through. A native before_tool_call deny would be a thinner version of the same problem: still per-call, still reasoning about cost after the decision to act.

The limit that actually holds lives in the harness, outside the loop, and gates on action class rather than cost — which operations are irreversible or expensive-by-category — with a hard stop between phases the runner enforces, not the model. Framed that way the question stops being “can I deny this call” and becomes “which classes need a gate, and where’s the phase boundary the agent can’t roll past on its own.”

Happy to share how I structure those phase gates if useful — spent a while getting the STOP-and-report boundary right for destructive jobs