step04 · Function Tools with Approvals
How to gate a sensitive tool behind human consent: the run pauses, hands you an approval request, and only calls the tool once you approve.
What this lesson demonstrates
The previous lesson let the model call the weather tool silently. This one puts a human in the loop. You wrap the tool with tool.ApprovalRequiredFunc, and now instead of invoking it, the run pauses and hands you a message.ToolApprovalRequestContent. You approve or deny, feed your decision back with a new run on the same session, and the agent either invokes the tool or skips it. This is the pattern for gating side-effecting or sensitive tools behind consent.
Marking a tool approval-required
The only change to the tool wiring is one wrapper:
Config: agent.Config{
Name: "WeatherAssistant",
// ApprovalRequiredFunc marks the tool as needing approval: the run will
// yield a ToolApprovalRequestContent instead of invoking it directly.
Tools: []tool.Tool{tool.ApprovalRequiredFunc(weatherTool)},
},
The weatherTool itself is an ordinary functool.MustNew function; ApprovalRequiredFunc decorates it so the model can’t fire it without your say-so.
The approval loop
In main, the first RunText comes back carrying approval requests, not an answer. A loop then drives consent to completion:
for {
userResponses := collectApprovals(resp, os.Stdin, os.Stdout)
if len(userResponses) == 0 {
return
}
resp, err = a.RunMessage(ctx, message.New(userResponses...),
agent.WithSession(session)).Collect()
demo.Print(resp, err)
}
collectApprovals scans the response’s contents for each *message.ToolApprovalRequestContent, prompts the user, and calls request.CreateResponse(approved, "") — which pairs your decision with the original request’s RequestID so the agent knows which pending call it answers. When a run yields no more requests, you’re done.
What to notice
The session is what makes this work. It ties the successive runs together so the agent remembers the pending tool call while it waits for your answer; drop agent.WithSession and the second run wouldn’t know what it’s approving. Note the second run uses RunMessage(message.New(userResponses...)), not RunText — you’re feeding structured approval content back in, not a text prompt.
The design is testable precisely because collectApprovals takes explicit in/out streams: a test can drive it with a scripted "y\n" and no real terminal, and the pure approval plumbing (approve → CreateResponse) is exercised offline.
How it maps to the Agent Framework
tool.ApprovalRequiredFunc and ToolApprovalRequestContent are the SDK’s human-in-the-loop primitives. The provider transports the approval request and response like any other message; the consent logic is yours. This is the same interruption model that later powers approvals inside group chats and workflows.
Run it
go run ./tutorial/02-agents/providers/foundry/step04_function_tools_with_approvals
The offline test drives the approval plumbing with a fake credential; the live run is gated behind AF_LIVE=1 with az login and FOUNDRY_PROJECT_ENDPOINT.
Key takeaways
tool.ApprovalRequiredFuncdecorates an ordinary function tool so the run pauses and yields amessage.ToolApprovalRequestContentinstead of invoking it — the pattern for side-effecting or sensitive tools.- Consent is a loop, not a callback.
collectApprovalsscans the response for each request, prompts the user, and callsrequest.CreateResponse(approved, ""), pairing the decision with the originalRequestID; you re-run withRunMessage(message.New(userResponses...))until no requests remain. - The session is what makes it work. It ties successive runs together so the agent remembers the pending call while it waits; drop
agent.WithSessionand the second run wouldn’t know what it’s approving. - Note the second run uses
RunMessage, notRunText— you’re feeding structured approval content back in, not a text prompt — and passing explicitin/outstreams keeps the loop testable offline with a scripted"y\n".
Further reading
- step03 · Function Tools (Foundry) — the ungated tool this lesson wraps.
- Human In Loop — the same approval primitive over the AG-UI transport.
- 21 · Shell with Environment — a tool whose default
ApprovalRequired()is the same consent boundary.
Next: step05 · Structured Output