Tool Confirmation
The confirmation protocol lets tools require human approval before executing. The framework provides the mechanism — consumers provide the UI/interaction layer.
Setup
Register a ConfirmationProvider on the agent. The provider is called whenever a tool requires confirmation and blocks until the consumer provides a decision.
myAgent := agent.New(llmClient,
agent.WithTools(&DeleteTool{}),
agent.WithConfirmationProvider(
func(ctx context.Context, req tool.ConfirmationRequest) (bool, error) {
// Present req to the user, wait for their decision
return askUser(req.ToolName, req.Input, req.Hint), nil
},
),
)
Return true to approve, false to reject. If the provider returns an error, the tool call fails with that error.
Declarative Confirmation
Set RequireConfirmation on a tool's Info to require approval before Run() is called:
func (t *DeleteTool) Info() tool.Info {
info := tool.NewInfo("delete_records", "Delete database records", DeleteParams{})
info.RequireConfirmation = true
return info
}
When the agent encounters this tool, it calls the ConfirmationProvider before executing. If no provider is configured, the tool runs normally — confirmation is opt-in.
Dynamic Confirmation
Tools can request confirmation from within Run() for conditional approval:
func (t *TransferTool) Run(ctx context.Context, params tool.Call) (tool.Response, error) {
var input TransferParams
json.Unmarshal([]byte(params.Input), &input)
if input.Amount > 10000 {
err := tool.RequestConfirmation(ctx, "Large transfer exceeding $10,000", input)
if err != nil {
return tool.Response{}, err
}
}
// Proceed with transfer
return tool.NewTextResponse("Transfer complete"), nil
}
RequestConfirmation blocks until the consumer decides. If rejected, it returns tool.ErrConfirmationRejected — propagate this error to halt execution. If no ConfirmationProvider is configured, RequestConfirmation is a no-op (auto-approve).
ConfirmationRequest
The provider receives a ConfirmationRequest with context about the tool call:
type ConfirmationRequest struct {
ToolCallID string // Unique ID of this tool call
ToolName string // Name of the tool
Input string // JSON-encoded arguments
Hint string // Human-readable description (dynamic confirmation only)
Payload any // Arbitrary structured data (dynamic confirmation only)
}
For declarative confirmation (RequireConfirmation flag), Hint and Payload are empty. For dynamic confirmation (RequestConfirmation), they carry the values passed by the tool.
Toolset-Level Confirmation
Use tool.WithConfirmation to mark all tools in a toolset as requiring confirmation:
dangerousTools := tool.NewToolset("dangerous",
&DeleteTool{},
&DropTableTool{},
&FormatDiskTool{},
)
confirmed := tool.WithConfirmation(dangerousTools)
myAgent := agent.New(llmClient,
agent.WithToolsets(confirmed),
agent.WithConfirmationProvider(myProvider),
)
This sets RequireConfirmation = true on every tool in the toolset without modifying the originals.
Streaming
In the streaming path (ChatStream), an EventConfirmationRequired event is emitted before the provider blocks. This allows the consumer to present a UI and then unblock the provider:
for event := range myAgent.ChatStream(ctx, "Delete old records") {
switch event.Type {
case types.EventConfirmationRequired:
req := event.ConfirmationRequest
fmt.Printf("Tool %q wants to run with input: %s\n", req.ToolName, req.Input)
// The provider is blocking — respond via whatever mechanism it uses
case types.EventContentDelta:
fmt.Print(event.Content)
case types.EventComplete:
fmt.Println("\nDone!")
}
}
A common pattern is to use a channel-based provider that the streaming consumer unblocks:
type approval struct {
approved bool
ch chan struct{}
}
pending := make(map[string]*approval)
var mu sync.Mutex
provider := func(ctx context.Context, req tool.ConfirmationRequest) (bool, error) {
a := &approval{ch: make(chan struct{})}
mu.Lock()
pending[req.ToolCallID] = a
mu.Unlock()
<-a.ch // Block until consumer decides
return a.approved, nil
}
// In the stream consumer, when EventConfirmationRequired arrives:
// mu.Lock()
// a := pending[req.ToolCallID]
// mu.Unlock()
// a.approved = userClickedApprove
// close(a.ch)
Interaction with Hooks
PreToolUse hooks run before confirmation. If a hook denies the tool, the confirmation provider is never called:
This means hooks enforce policy (rate limits, blocklists), while confirmation handles human approval.
Handoffs
Each agent has its own ConfirmationProvider. When a handoff occurs, the new agent's provider is used. If the target agent has no provider, its tools run without confirmation.
Auto-Approve Patterns
The provider is a regular function — implement any approval logic:
// Always approve (useful for testing)
agent.WithConfirmationProvider(
func(_ context.Context, _ tool.ConfirmationRequest) (bool, error) {
return true, nil
},
)
// Check a database of pre-approved tools
agent.WithConfirmationProvider(
func(ctx context.Context, req tool.ConfirmationRequest) (bool, error) {
return db.IsToolPreApproved(ctx, userID, req.ToolName)
},
)
// Approve safe tools, prompt for dangerous ones
agent.WithConfirmationProvider(
func(ctx context.Context, req tool.ConfirmationRequest) (bool, error) {
if req.ToolName == "read_file" {
return true, nil
}
return promptUser(ctx, req)
},
)