Some tools should not run without a human’s sign-off. Mark a tool as requiring approval and the run pauses before executing it: you get the pending calls back, record approve/reject decisions, and resume — in the same process or, via serialization, in a completely different one.
deleteRepo := agents.NewFunctionTool("delete_repo", "Permanently delete a repository.",
func(ctx context.Context, tc *agents.ToolContext, args deleteArgs) (string, error) {
return doDelete(args.Name)
})
deleteRepo.NeedsApproval = true
Or decide per call from the arguments:
deleteRepo.NeedsApprovalFunc = func(ctx context.Context, rc *agents.RunContext, argsJSON string) (bool, error) {
return strings.Contains(argsJSON, `"prod"`), nil // only prod deletions need approval
}
When the model requests an approval-gated tool, Run returns without executing anything from that turn — so nothing runs twice after resumption:
res, err := agents.Run(ctx, agent, "delete the prod repo", opts)
if err != nil {
log.Fatal(err)
}
for len(res.Interruptions) > 0 {
for _, item := range res.Interruptions {
fmt.Printf("approve %s(%s)? ", item.ToolName, item.Arguments)
if askHuman() {
res.State.Approve(item, false)
} else {
res.State.Reject(item, false, "denied by operator")
}
}
res, err = agents.ResumeRun(ctx, res.State, opts)
if err != nil {
log.Fatal(err)
}
}
fmt.Println(res.FinalOutputString())
RunResult.Interruptions lists the pending calls (ToolName, CallID, raw Arguments).RunResult.State is the resumable *RunState; record decisions on it with Approve / Reject.Reject’s message (default: "Tool execution was not approved.") is sent to the model as the tool output so it can adapt.always=true to Approve/Reject to apply the decision to every future call of that tool in the run.Events(), then check FinalResult() and resume with ResumeRun.RunState serializes to JSON, so the approval can happen hours later in another process (a ticket queue, a Slack button, …):
// Process A: pause and persist
data, _ := json.Marshal(res.State)
store.Save(runID, data)
// Process B: restore, decide, resume
state, err := agents.RunStateFromJSON(data, map[string]*agents.Agent{
"assistant": assistant, // every agent that participated, by name
})
if err != nil { … }
state.Approve(state.Interruptions[0], false)
res, err := agents.ResumeRun(ctx, state, opts)
Because Go functions don’t serialize, RunStateFromJSON needs a registry mapping agent names back to your *Agent values. The format round-trips Go↔Go only — it is not compatible with the Python SDK’s RunState JSON.
When the run uses a Session, nothing is saved at the interruption point; the user input and all generated items are persisted once the resumed run completes. Pass the same Session in ResumeRun’s options.