Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
28 changes: 28 additions & 0 deletions examples/tools/confirmation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Confirmation Example

This example demonstrates how to use the confirmation feature in FunctionTools.

## Features Demonstrated

1. **Static Confirmation**: Using `RequireConfirmation: true` in the tool config to always require confirmation
2. **Dynamic Confirmation**: Using `ctx.RequestConfirmation()` within the tool function to request confirmation at runtime

## Running the Example

```bash
go run main.go
```

## How It Works

1. The example creates two types of tools that perform "file write" operations:
- A tool that uses static confirmation (defined in config)
- A tool that requests confirmation dynamically (in the function)

2. When either tool is called by the LLM:
- The static confirmation tool will always show a note in its description that it requires confirmation
- The dynamic confirmation tool will pause execution when `RequestConfirmation` is called

3. The confirmation request includes the tool name, a hint, and any associated payload data

This is useful for safety-critical operations like file system modifications or system commands that should require user approval before execution.
106 changes: 106 additions & 0 deletions examples/tools/confirmation/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package main demonstrates the use of confirmation in FunctionTools.
package main

import (
"context"
"fmt"
"log"
"os"

"google.golang.org/adk/agent"
"google.golang.org/adk/llm"
"google.golang.org/adk/model"
"google.golang.org/adk/runner/full"
"google.golang.org/adk/session"
"google.golang.org/adk/tool"
"google.golang.org/adk/tool/functiontool"
)

type WriteFileArgs struct {
Filename string `json:"filename"`
Content string `json:"content"`
}

// writeWithConfirmation simulates a file write operation that requires confirmation
func writeWithConfirmation(ctx tool.Context, args WriteFileArgs) (string, error) {
// Request confirmation before writing the file
err := ctx.RequestConfirmation("Writing file: "+args.Filename, map[string]any{
"filename": args.Filename,
"content": args.Content,
})
if err != nil {
// If confirmation was required, the flow will pause and wait for confirmation
// In this example, we would normally resume after receiving confirmation
return "", err
}

// After confirmation is granted, we would write the file
// For this example, we'll just simulate it
return fmt.Sprintf("File %s written successfully", args.Filename), nil
}

// staticConfirmationTool is a file operation that always requires confirmation
func staticConfirmationTool(ctx tool.Context, args WriteFileArgs) (string, error) {
return fmt.Sprintf("Static confirmation tool executed for file: %s", args.Filename), nil
}

func main() {
ctx := context.Background()

// Create a function tool that requests confirmation dynamically
dynamicTool, err := functiontool.New(functiontool.Config{
Name: "write_file_dynamic",
Description: "Write content to a file, with dynamic confirmation",
}, writeWithConfirmation)
if err != nil {
log.Fatalf("Failed to create dynamic confirmation tool: %v", err)
}

// Create a function tool that always requires confirmation via config
staticTool, err := functiontool.New(functiontool.Config{
Name: "write_file_static",
Description: "Write content to a file, with static confirmation requirement",
RequireConfirmation: true,
}, staticConfirmationTool)
if err != nil {
log.Fatalf("Failed to create static confirmation tool: %v", err)
}

// Create a simple LLM agent with the tools
llmAgent, err := agent.NewLLMAgent(ctx, agent.LLMAgentConfig{
Name: "file_operator",
Model: &model.MockModel{}, // Use mock model for example
Tools: []tool.Tool{dynamicTool, staticTool},
})
if err != nil {
log.Fatalf("Failed to create agent: %v", err)
}

// Create a runner config
config := &session.SessionConfig{
Agents: []agent.Agent{llmAgent},
}

// Create a launcher with full capabilities
l := full.NewLauncher()

// Run the agent
err = l.ParseAndRun(ctx, config, os.Args[1:], nil)
if err != nil {
log.Fatalf("Failed to run: %v", err)
}
}
53 changes: 37 additions & 16 deletions internal/llminternal/base_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ var (
DefaultResponseProcessors = []func(ctx agent.InvocationContext, req *model.LLMRequest, resp *model.LLMResponse) error{
nlPlanningResponseProcessor,
codeExecutionResponseProcessor,
confirmationRequestProcessor,
}
)

Expand Down Expand Up @@ -229,7 +230,7 @@ func toolPreprocess(ctx agent.InvocationContext, req *model.LLMRequest, tools []
return fmt.Errorf("tool %q does not implement RequestProcessor() method", t.Name())
}
// TODO: how to prevent mutation on this?
toolCtx := toolinternal.NewToolContext(ctx, "", &session.EventActions{})
toolCtx := toolinternal.NewToolContextWithToolName(ctx, "", &session.EventActions{}, t.Name())
if err := requestProcessor.ProcessRequest(toolCtx, req); err != nil {
return err
}
Expand Down Expand Up @@ -381,26 +382,46 @@ func (f *Flow) handleFunctionCalls(ctx agent.InvocationContext, toolsDict map[st

result := f.callTool(funcTool, fnCall.Args, toolCtx)

// TODO: agent.canonical_after_tool_callbacks
// TODO: handle long-running tool.
// Check if confirmation was requested
ev := session.NewEvent(ctx.InvocationID())
ev.LLMResponse = model.LLMResponse{
Content: &genai.Content{
Role: "user",
Parts: []*genai.Part{
{
FunctionResponse: &genai.FunctionResponse{
ID: fnCall.ID,
Name: fnCall.Name,
Response: result,
ev.Author = ctx.Agent().Name()
ev.Branch = ctx.Branch()
ev.Actions = *toolCtx.Actions()
if toolCtx.Actions().ConfirmationRequest != nil {
// If confirmation is requested, we need to return an event with the confirmation request
// Set a special status to indicate that confirmation is required
ev.LLMResponse = model.LLMResponse{
Content: &genai.Content{
Role: "user",
Parts: []*genai.Part{
{
Text: fmt.Sprintf("Confirmation required for tool %s: %s", fnCall.Name, toolCtx.Actions().ConfirmationRequest.Hint),
},
},
},
// Add custom metadata to indicate this is a confirmation request
CustomMetadata: map[string]any{
"confirmation_required": true,
"confirmation_request": toolCtx.Actions().ConfirmationRequest,
},
}
} else {
// Normal response when no confirmation needed
ev.LLMResponse = model.LLMResponse{
Content: &genai.Content{
Role: "user",
Parts: []*genai.Part{
{
FunctionResponse: &genai.FunctionResponse{
ID: fnCall.ID,
Name: fnCall.Name,
Response: result,
},
},
},
},
},
}
}
ev.Author = ctx.Agent().Name()
ev.Branch = ctx.Branch()
ev.Actions = *toolCtx.Actions()
telemetry.TraceToolCall(spans, curTool, fnCall.Args, ev)
fnResponseEvents = append(fnResponseEvents, ev)
}
Expand Down
13 changes: 13 additions & 0 deletions internal/llminternal/other_processors.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,16 @@ func codeExecutionResponseProcessor(ctx agent.InvocationContext, req *model.LLMR
// TODO: implement (adk-python src/google/adk_code_execution.py)
return nil
}

func confirmationRequestProcessor(ctx agent.InvocationContext, req *model.LLMRequest, resp *model.LLMResponse) error {
// This processor is a placeholder for handling confirmation requests that may originate
// from the LLM response. The primary logic for handling tool-initiated confirmation
// requests is handled in the base_flow.
if _, ok := resp.CustomMetadata["confirmation_request"]; ok {
if req.Tools != nil {
// The confirmation request would be handled at a higher level.
// Currently, this is a no-op.
}
}
return nil
}
36 changes: 36 additions & 0 deletions internal/toolinternal/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package toolinternal

import (
"context"
"fmt"

"github.com/google/uuid"
"google.golang.org/genai"
Expand Down Expand Up @@ -49,6 +50,11 @@ func (ia *internalArtifacts) Save(ctx context.Context, name string, data *genai.
}

func NewToolContext(ctx agent.InvocationContext, functionCallID string, actions *session.EventActions) tool.Context {
return NewToolContextWithToolName(ctx, functionCallID, actions, "")
}

// NewToolContextWithToolName creates a new tool context with the ability to track the tool name
func NewToolContextWithToolName(ctx agent.InvocationContext, functionCallID string, actions *session.EventActions, toolName string) tool.Context {
if functionCallID == "" {
functionCallID = uuid.NewString()
}
Expand All @@ -69,6 +75,7 @@ func NewToolContext(ctx agent.InvocationContext, functionCallID string, actions
Artifacts: ctx.Artifacts(),
eventActions: actions,
},
toolName: toolName,
}
}

Expand All @@ -78,6 +85,7 @@ type toolContext struct {
functionCallID string
eventActions *session.EventActions
artifacts *internalArtifacts
toolName string
}

func (c *toolContext) Artifacts() agent.Artifacts {
Expand All @@ -99,3 +107,31 @@ func (c *toolContext) AgentName() string {
func (c *toolContext) SearchMemory(ctx context.Context, query string) (*memory.SearchResponse, error) {
return c.invocationContext.Memory().Search(ctx, query)
}

func (c *toolContext) RequestConfirmation(hint string, payload map[string]any) error {
// Create a confirmation request
confirmationID := uuid.NewString()
toolName := c.toolName
if toolName == "" {
// A meaningful tool name is required for a confirmation request.
return fmt.Errorf("tool name is missing in context for confirmation request with hint: %s", hint)
}

request := tool.ConfirmationRequest{
ID: confirmationID,
ToolName: toolName,
Hint: hint,
Payload: payload,
}

// Store the confirmation request in the event actions
if c.eventActions.ConfirmationRequest == nil {
c.eventActions.ConfirmationRequest = &request
} else {
// If there's already a confirmation request, return an error
return fmt.Errorf("a confirmation request is already pending")
}

// Return a specific error to indicate that confirmation is required
return fmt.Errorf("confirmation required for action: %s", hint)
}
62 changes: 62 additions & 0 deletions internal/toolinternal/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,65 @@ func TestToolContext(t *testing.T) {
t.Errorf("ToolContext(%+T) is unexpectedly an InvocationContext", got)
}
}

func TestToolContext_Confirmation(t *testing.T) {
inv := contextinternal.NewInvocationContext(t.Context(), contextinternal.InvocationContextParams{})
actions := &session.EventActions{}
toolCtx := NewToolContextWithToolName(inv, "fn1", actions, "test_tool")

hint := "This is a test confirmation"
payload := map[string]any{"key": "value"}

// Initially, no confirmation should be requested
if actions.ConfirmationRequest != nil {
t.Errorf("ConfirmationRequest should be nil initially, got: %v", actions.ConfirmationRequest)
}

// Request confirmation
err := toolCtx.RequestConfirmation(hint, payload)
if err == nil {
t.Errorf("Expected RequestConfirmation to return an error to indicate confirmation is required")
}

// Check that confirmation request was stored in actions
if actions.ConfirmationRequest == nil {
t.Error("ConfirmationRequest should not be nil after calling RequestConfirmation")
} else {
if actions.ConfirmationRequest.Hint != hint {
t.Errorf("Expected hint %q, got %q", hint, actions.ConfirmationRequest.Hint)
}
if actions.ConfirmationRequest.ToolName != "test_tool" {
t.Errorf("Expected tool name %q, got %q", "test_tool", actions.ConfirmationRequest.ToolName)
}
if len(actions.ConfirmationRequest.Payload) != 1 || actions.ConfirmationRequest.Payload["key"] != "value" {
t.Errorf("Payload was not stored correctly: %v", actions.ConfirmationRequest.Payload)
}
}

// Try to request another confirmation - should fail
err = toolCtx.RequestConfirmation("Another request", map[string]any{})
if err == nil {
t.Error("Expected second call to RequestConfirmation to fail")
}
}
Comment on lines +40 to +79
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This test covers the primary functionality of RequestConfirmation well. To improve test coverage, consider adding a test case for the fallback logic in RequestConfirmation where toolName is not provided during context creation. This would ensure the behavior for that edge case is tested and correct.


func TestToolContext_ConfirmationNoToolName(t *testing.T) {
inv := contextinternal.NewInvocationContext(t.Context(), contextinternal.InvocationContextParams{})
actions := &session.EventActions{}
toolCtx := NewToolContext(inv, "fn1", actions)

hint := "This is a test confirmation"
payload := map[string]any{"key": "value"}

// Request confirmation - should fail because tool name is missing
err := toolCtx.RequestConfirmation(hint, payload)
if err == nil {
t.Error("Expected RequestConfirmation to return an error when tool name is missing")
}

// Check that no confirmation request was stored
if actions.ConfirmationRequest != nil {
t.Error("ConfirmationRequest should be nil when RequestConfirmation fails")
}
}

9 changes: 8 additions & 1 deletion session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/google/uuid"

"google.golang.org/adk/model"
"google.golang.org/adk/tool"
)

// Session represents a series of interactions between a user and agents.
Expand Down Expand Up @@ -125,7 +126,10 @@ func (e *Event) IsFinalResponse() bool {
return true
}

return !hasFunctionCalls(&e.LLMResponse) && !hasFunctionResponses(&e.LLMResponse) && !e.LLMResponse.Partial && !hasTrailingCodeExecutionResult(&e.LLMResponse)
// Check if there's a confirmation request - if so, this is not a final response
hasConfirmationRequest := e.Actions.ConfirmationRequest != nil

return !hasFunctionCalls(&e.LLMResponse) && !hasFunctionResponses(&e.LLMResponse) && !e.LLMResponse.Partial && !hasTrailingCodeExecutionResult(&e.LLMResponse) && !hasConfirmationRequest
}

// NewEvent creates a new event defining now as the timestamp.
Expand Down Expand Up @@ -154,6 +158,9 @@ type EventActions struct {
TransferToAgent string
// The agent is escalating to a higher level agent.
Escalate bool

// If set, indicates that a confirmation is required before proceeding.
ConfirmationRequest *tool.ConfirmationRequest
}

// Prefixes for defining session's state scopes
Expand Down
Loading