์ฝ˜ํ…์ธ ๋กœ ์ด๋™

Human input for agent workflows

ADK์—์„œ ์ง€์›Python v2.0.0TypeScript v2.0.0Go v2.0.0

Being able to request human input for data input, decision verification, or action permission is an important part of many agent-powered workflows. Graph-based workflows in ADK can include human in the loop (HITL) nodes specifically built for obtaining input from humans as part of a workflow. These nodes do not require artificial intelligence (AI) models to run, which can make the input process more predictable and reliable.

์‹œ์ž‘ํ•˜๊ธฐ

You can implement a human input node in a graph using the RequestInput class and a text prompt for the user. The following code example shows how to add a human input node to a Workflow graph:

from google.adk.events import RequestInput
from google.adk import Workflow

def step1(): # Human input step
  yield RequestInput(message="Enter a number:")

def step2(node_input):
  return node_input * 2

root_agent = Workflow(
    name="root_agent",
    edges=[('START', step1, step2)],
)

In this code example, step1 pauses the execution of the agent until the system receives an input from a user. Once the system receives input from the user, that input is passed to the next node.

ADK TypeScript v2.0.0์—์„œ ์ธ๊ฐ„ ์ž…๋ ฅ ๋…ธ๋“œ๋Š” RequestInput์„ yieldํ•ฉ๋‹ˆ๋‹ค. step1 ๋…ธ๋“œ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์‘๋‹ตํ•  ๋•Œ๊นŒ์ง€ ์›Œํฌํ”Œ๋กœ๋ฅผ ์ผ์‹œ ์ค‘์ง€ํ•˜๊ณ , ์‚ฌ์šฉ์ž์˜ ์‘๋‹ต์€ ๋‹ค์Œ ๋…ธ๋“œ์˜ ์ž…๋ ฅ์œผ๋กœ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค. ์ธ๊ฐ„ ์ฐธ์—ฌํ˜•(HITL) ๋…ธ๋“œ๋Š” ๋ชจ๋ธ์„ ํ•„์š”๋กœ ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์ผ์‹œ ์ค‘์ง€๊ฐ€ ๊ฒฐ์ •๋ก ์ ์ž…๋‹ˆ๋‹ค.

import { node, NodeContext, RequestInput, Workflow } from '@google/adk';

const step1 = node(
  async function* () {
    yield new RequestInput({ message: 'Enter a number:' });
  },
  { name: 'step1' },
);

const step2 = node(
  (_ctx: NodeContext, nodeInput: string | number) => {
    const value = Number(nodeInput);
    return Number.isFinite(value)
      ? value * 2
      : `"${nodeInput}" is not a number.`;
  },
  { name: 'step2' },
);

export const rootAgent = new Workflow({
  name: 'root_agent',
  edges: [['START', step1, step2]],
});

์ด ๊ตฌํ˜„์€ ๊ธฐ๋ณธ๊ฐ’์ธ rerunOnResume: false ํ•ธ๋“œ์˜คํ”„๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. ์ค‘๋‹จ๋œ ๋…ธ๋“œ๋Š” ๋‹ค์‹œ ์‹คํ–‰๋˜์ง€ ์•Š์œผ๋ฉฐ, ์‚ฌ์šฉ์ž์˜ ์‘๋‹ต์„ ์ถœ๋ ฅ์œผ๋กœ ์‚ผ์•„ ์™„๋ฃŒ๋ฉ๋‹ˆ๋‹ค. ctx.runNode()๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋…ธ๋“œ๋Š” ๋Œ€์‹  rerunOnResume: true๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์ž์„ธํ•œ ๋‚ด์šฉ์€ ๋™์  ์›Œํฌํ”Œ๋กœ์—์„œ์˜ ์ธ๊ฐ„ ์ž…๋ ฅ์„ ์ฐธ์กฐํ•˜์„ธ์š”.

In ADK Go v2.0.0, a HITL graph node is built with workflow.NewEmittingFunctionNode and workflow.ResumeOrRequestInput. This is the direct equivalent of Python's RequestInput node:

  • On the first pass, workflow.ResumeOrRequestInput emits a session.RequestInput event (surfaced as Event.RequestedInput) and returns ErrNodeInterrupted, pausing the workflow.
  • After the human replies, the node is re-invoked from the top (RerunOnResume: &true) and ResumeOrRequestInput returns the reply payload, which flows as typed input to the next node via event.Output.
// newGraphHITLWorkflow demonstrates a graph HITL node using
// workflow.NewEmittingFunctionNode and workflow.ResumeOrRequestInput.
//
// This is the Go equivalent of the Python RequestInput node:
//
//  def step1():  # Human input step
//      yield RequestInput(message="Enter a number:")
//
//  def step2(node_input):
//      return node_input * 2
//
//  root_agent = Workflow(
//      name="root_agent",
//      edges=[('START', step1, step2)],
//  )
//
// On the first pass, step1Node emits a RequestInput event and pauses the
// workflow (ErrNodeInterrupted). After the human replies, the node is re-run
// and ResumeOrRequestInput returns the reply, which flows as typed input to
// step2Node via event.Output.
func newGraphHITLWorkflow() (agent.Agent, error) {
    rerun := true

    // step1Node: pauses for human input on the first pass, returns the
    // human's reply on resume. workflow.ResumeOrRequestInput handles both
    // phases โ€” no manual re-entry bookkeeping needed.
    step1Node := workflow.NewEmittingFunctionNode[any, string]("step1",
        func(ctx agent.Context, _ any, emit func(*session.Event) error) (string, error) {
            reply, err := workflow.ResumeOrRequestInput(ctx, emit, session.RequestInput{
                InterruptID: "enter_number",
                Message:     "Enter a number:",
            })
            if err != nil {
                // ErrNodeInterrupted on first pass โ€” workflow pauses here.
                return "", err
            }
            // On resume, reply is the human's text response.
            number, _ := reply.(string)
            return number, nil
        },
        workflow.NodeConfig{RerunOnResume: &rerun},
    )

    // step2Node: receives the human's input as its typed string input via
    // event.Output and doubles the number.
    step2Node := workflow.NewFunctionNode("step2",
        func(_ agent.Context, input string) (string, error) {
            return fmt.Sprintf("You entered: %s (doubled: %s%s)", input, input, input), nil
        },
        workflow.NodeConfig{},
    )

    return workflowagent.New(workflowagent.Config{
        Name:        "root_agent",
        Description: "Pauses for a number from the user, then doubles it.",
        Edges:       workflow.Chain(workflow.Start, step1Node, step2Node),
    })
}

๊ตฌ์„ฑ ์˜ต์…˜

Human input nodes can use the RequestInput class with the following configuration options:

  • message: Text provided to the user to explain the human input request.
  • payload: Structured data to be used as part of the human input request.
  • response_schema: A data structure the human response must conform to.

RequestInput ํด๋ž˜์Šค๋Š” ๋‹ค์Œ ๊ตฌ์„ฑ ์˜ต์…˜์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค:

  • message: ์‚ฌ์šฉ์ž์—๊ฒŒ ์งˆ๋ฌธ ๋‚ด์šฉ์„ ์„ค๋ช…ํ•˜๋Š” ํ…์ŠคํŠธ์ž…๋‹ˆ๋‹ค.
  • payload: ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ถ”๊ฐ€ ์ปจํ…์ŠคํŠธ๋ฅผ ๋ Œ๋”๋งํ•  ์ˆ˜ ์žˆ๋„๋ก ํ”„๋กฌํ”„ํŠธ์™€ ํ•จ๊ป˜ ์ „์†ก๋˜๋Š” ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ์ž…๋‹ˆ๋‹ค.
  • responseSchema: ์‘๋‹ต์ด ์ทจํ•  ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ๋˜๋Š” ํ˜•ํƒœ์ž…๋‹ˆ๋‹ค. ์Šคํ‚ค๋งˆ๋Š” ์ธํ„ฐ๋ŸฝํŠธ ์‹œ functionCall.args.response_schema๋กœ ์ „๋‹ฌ๋˜๋ฉฐ, ํด๋ผ์ด์–ธํŠธ๋Š” ์ด๋ฅผ ์ฝ์–ด ์‘๋‹ต์šฉ ์–‘์‹์„ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.

๋…ธ๋“œ์˜ rerunOnResume ์˜ต์…˜์€ ์‘๋‹ต์ด ๋„์ฐฉํ–ˆ์„ ๋•Œ ์ผ์–ด๋‚˜๋Š” ๋™์ž‘์„ ์ œ์–ดํ•ฉ๋‹ˆ๋‹ค:

  • false (๋ฆฌํ”„ ๋…ธ๋“œ ๊ธฐ๋ณธ๊ฐ’): ์‘๋‹ต์ด ์ค‘๋‹จ๋œ ๋…ธ๋“œ๋ฅผ ์šฐํšŒํ•˜์—ฌ ํ•ด๋‹น ๋…ธ๋“œ์˜ ํ›„์† ๋…ธ๋“œ์— ์ž…๋ ฅ์œผ๋กœ ๋ผ์šฐํŒ…๋ฉ๋‹ˆ๋‹ค.
  • true: ๋…ธ๋“œ ๋ณธ๋ฌธ์ด ์ฒ˜์Œ๋ถ€ํ„ฐ ๋‹ค์‹œ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ์ด ์„ค์ •์€ ctx.runNode()๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋ชจ๋“  ๋…ธ๋“œ์— ํ•„์š”ํ•˜๋ฉฐ, ์žฌ๊ฐœ ์‹œ ์บ์‹œ๋œ ํ•˜์œ„ ๊ฒฐ๊ณผ๋ฅผ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

session.RequestInput carries the following fields, which map directly to Python's RequestInput parameters:

  • InterruptID (string): A unique identifier for this pause point. Use a stable prefix plus a UUID to avoid collision across workflow runs. Equivalent to the implicit interrupt ID in Python.
  • Message (string): Human-readable prompt displayed to the user. Equivalent to Python's message parameter.
  • Payload (any): Optional structured data sent alongside the prompt so the client can render additional context. Equivalent to Python's payload parameter.

workflow.NodeConfig.RerunOnResume controls what happens on resume:

  • &true: the node body is re-run from the top; ResumeOrRequestInput returns the human's reply on the second pass. Required for nodes that use ResumeOrRequestInput.
  • &false or nil (leaf default): the reply is routed to the node's successor as input, bypassing the interrupted node.

Note: Structured response from the client

ADK Go does not automatically parse or validate the structure of the human's reply payload. If your workflow needs structured feedback, include a UI or a downstream agent node to validate the response before acting on it.

์ฐธ๊ณ : ์‘๋‹ต ์Šคํ‚ค๋งˆ ์ž…๋ ฅ ์ œํ•œ์‚ฌํ•ญ

์‘๋‹ต ์Šคํ‚ค๋งˆ๋Š” ์‚ฌ๋žŒ์˜ ์‘๋‹ต์„ ์ง€์ •๋œ ๊ตฌ์กฐ์— ๋งž๊ฒŒ ์ž๋™์œผ๋กœ ์žฌ๊ตฌ์„ฑํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์‘๋‹ต์€ ์ด๋ฏธ ํ•ด๋‹น ํ˜•์‹์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋” ๋‚˜์€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์œ„ํ•ด ํด๋ผ์ด์–ธํŠธ ์ธํ„ฐํŽ˜์ด์Šค์—์„œ ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•˜๊ฑฐ๋‚˜, ์ผ์‹œ ์ค‘์ง€ ๋’ค์— ์—์ด์ „ํŠธ ๋…ธ๋“œ๋ฅผ ๋ฐฐ์น˜ํ•˜์—ฌ ์‘๋‹ต์„ ํ•„์š”ํ•œ ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ•˜์„ธ์š”.

์‚ฌ๋žŒ์˜ ์ž…๋ ฅ (Human Input) examples

The following code examples demonstrate more detailed human input requests.

Request input with a message and payload

The following code sample shows how to construct a RequestInput object in a workflow node, including a payload and response schema. In this example, the ActivitiesList is expected to be completed by an agent node that composes a list of activities, and the get_user_feedback() node requests feedback from the user.

class ActivitiesList(BaseModel):
   """Itinerary should be a list of dictionaries for each activity. Each
   activity has a name and a description"""
   itinerary: List[Dict[str, str]]

class UserFeedback(BaseModel):
   """Expected response structure from the user."""
   user_response: str

async def get_user_feedback(node_input: ActivitiesList):
   """
   Retrieves the user's thoughts on the agents initial itinerary in order to
   either expand on, change the list, or exit the loop
   """
   message = (
       f"""
       Here is your recommended base itinerary:\n{node_input}\n\n
       Which of these items appeal to you (if any)?
       """
   )

   yield RequestInput(
       message=message,
       payload=node_input,
        response_schema=UserFeedback,
   )

๋‹ค์Œ 3๊ฐœ ๋…ธ๋“œ ๊ทธ๋ž˜ํ”„๋Š” ๊ตฌ์กฐํ™”๋œ ์—ฌํ–‰ ์ผ์ •์„ ์ž‘์„ฑํ•˜๊ณ , ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ด๋ฅผ ๋ Œ๋”๋งํ•  ์ˆ˜ ์žˆ๋„๋ก ํ”„๋กฌํ”„ํŠธ์™€ ํ•จ๊ป˜ payload๋กœ ์ „์†กํ•œ ๋‹ค์Œ ์‚ฌ์šฉ์ž์˜ ํ”ผ๋“œ๋ฐฑ์— ๋”ฐ๋ผ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค:

import { node, NodeContext, RequestInput, Workflow } from '@google/adk';
import { z } from 'zod';

/**
 * Itinerary is a list of activities. Each activity has a name and a
 * description.
 */
const activitiesListSchema = z.object({
  itinerary: z.array(z.object({ name: z.string(), description: z.string() })),
});
type ActivitiesList = z.infer<typeof activitiesListSchema>;

/** Expected response structure from the user. */
const userFeedbackSchema = z.object({
  userResponse: z.string(),
});

const buildItinerary = node(
  (_ctx: NodeContext, city: string): ActivitiesList => {
    const place = city.trim() || 'your city';
    return {
      itinerary: [
        { name: 'Morning walk', description: `A stroll through old ${place}.` },
        { name: 'Local lunch', description: `Regional food in ${place}.` },
        { name: 'Museum visit', description: `The main museum of ${place}.` },
      ],
    };
  },
  { name: 'build_itinerary', outputSchema: activitiesListSchema },
);

/**
 * Retrieves the user's thoughts on the agent's initial itinerary in order to
 * either expand on it, change the list, or exit the loop.
 */
const getUserFeedback = node(
  async function* (_ctx: NodeContext, nodeInput: ActivitiesList) {
    const rendered = nodeInput.itinerary
      .map((a, i) => `  ${i + 1}. ${a.name} โ€” ${a.description}`)
      .join('\n');

    yield new RequestInput({
      message:
        `Here is your recommended base itinerary:\n${rendered}\n\n` +
        'Which of these items appeal to you (if any)?',
      payload: nodeInput,
      responseSchema: userFeedbackSchema,
    });
  },
  { name: 'get_user_feedback' },
);

const applyFeedback = node(
  (_ctx: NodeContext, nodeInput: unknown) => {
    const feedback =
      typeof nodeInput === 'string'
        ? nodeInput
        : String(
            (nodeInput as { userResponse?: unknown } | null)?.userResponse ??
              JSON.stringify(nodeInput),
          );
    return `Noted. Building the final itinerary around: ${feedback}`;
  },
  { name: 'apply_feedback' },
);

export const rootAgent = new Workflow({
  name: 'concierge_workflow',
  edges: [['START', buildItinerary, getUserFeedback, applyFeedback]],
});

The following code sample shows a three-node graph: a builder node generates a structured itinerary, a HITL node sends it as Payload alongside the prompt, and a final node acts on the user's feedback. The Payload field lets the client render the full itinerary for the user before they respond:

// ItineraryItem represents a single activity in a travel plan.
type ItineraryItem struct {
    Name        string `json:"name"`
    Description string `json:"description"`
}

// newItineraryReviewWorkflow demonstrates a graph HITL node that sends a
// structured payload alongside the input prompt so the client can render
// additional context for the user. This mirrors Python's:
//
//  async def get_user_feedback(node_input: ActivitiesList):
//      yield RequestInput(
//          message="Which items appeal to you?",
//          payload=node_input,
//          response_schema=UserFeedback,
//      )
func newItineraryReviewWorkflow() (agent.Agent, error) {
    rerun := true

    // buildItineraryNode: generates an itinerary and passes it to the HITL
    // node as its typed output via event.Output.
    buildItineraryNode := workflow.NewFunctionNode("build_itinerary",
        func(_ agent.Context, _ any) ([]ItineraryItem, error) {
            return []ItineraryItem{
                {Name: "Eiffel Tower", Description: "Iconic iron lattice tower."},
                {Name: "Louvre Museum", Description: "World's largest art museum."},
                {Name: "Seine River Cruise", Description: "Scenic boat tour of Paris."},
            }, nil
        },
        workflow.NodeConfig{},
    )

    // reviewNode: sends the itinerary as payload alongside the prompt so the
    // client can display it. On resume, the human's selection is returned.
    reviewNode := workflow.NewEmittingFunctionNode[[]ItineraryItem, string]("get_user_feedback",
        func(ctx agent.Context, itinerary []ItineraryItem, emit func(*session.Event) error) (string, error) {
            reply, err := workflow.ResumeOrRequestInput(ctx, emit, session.RequestInput{
                InterruptID: "itinerary_review",
                Message:     fmt.Sprintf("Here is your recommended itinerary (%d activities). Which items appeal to you?", len(itinerary)),
                Payload:     itinerary, // structured payload rendered by the client
            })
            if err != nil {
                // ErrNodeInterrupted on first pass โ€” workflow pauses here.
                return "", err
            }
            feedback, _ := reply.(string)
            return feedback, nil
        },
        workflow.NodeConfig{RerunOnResume: &rerun},
    )

    // finalNode: receives the user's feedback and produces a confirmation.
    finalNode := workflow.NewFunctionNode("finalize",
        func(_ agent.Context, feedback string) (string, error) {
            return fmt.Sprintf("Itinerary finalised with your feedback: %q", feedback), nil
        },
        workflow.NodeConfig{},
    )

    return workflowagent.New(workflowagent.Config{
        Name:        "concierge_workflow",
        Description: "Builds an itinerary, asks the user for feedback, then finalises.",
        Edges:       workflow.Chain(workflow.Start, buildItineraryNode, reviewNode, finalNode),
    })
}

๋„๊ตฌ ํ™•์ธ: LLM ์—์ด์ „ํŠธ์˜ ์Šน์ธ ํ”„๋กฌํ”„ํŠธ

Tool-confirmation is a separate, LLM-agentโ€“level mechanism for yes/no approval prompts. Unlike graph HITL nodes, tool-confirmation works inside an llmagent tool function rather than as a standalone graph node. It is useful when you want an LLM agent to pause and ask for approval before executing a specific tool call.

The following code sample shows how to construct a RequestInput object in a workflow node, including a response schema:

async def initial_prompt(ctx: Context):
   """Ask the user for itinerary information"""
   input_message = """
       This is an interactive concierge workflow tasked with making you a great
       itinerary for you in your city of choice. If you give some details about
       yourself or what you are generally looking for I can better personalize
       your itinerary.
       For example, input your:
           City (Required),
           Age,
           Hobby,
           Example of attraction you liked
   """
    yield RequestInput(message=input_message, response_schema=str)

๋„๊ตฌ๊ฐ€ ์‹คํ–‰๋˜๊ธฐ ์ „์— ์—์ด์ „ํŠธ๊ฐ€ ์Šน์ธ์„ ์œ„ํ•ด ์ผ์‹œ ์ค‘์ง€ํ•˜๋„๋ก ํ•˜๋ ค๋ฉด FunctionTool์— requireConfirmation: true๋ฅผ ์„ค์ •ํ•˜์„ธ์š”. ๊ทธ๋ž˜ํ”„์˜ ์ธ๊ฐ„ ์ฐธ์—ฌํ˜•(HITL) ๋…ธ๋“œ๋Š” ๋‹ค๋ฅธ ๋ชฉ์ ์œผ๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ๋„๊ตฌ ํ˜ธ์ถœ์„ ํ™•์ธํ•˜๋Š” ๋Œ€์‹  ์‚ฌ์šฉ์ž์—๊ฒŒ ์ž…๋ ฅ์„ ์š”์ฒญํ•˜์—ฌ ์›Œํฌํ”Œ๋กœ๋ฅผ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. responseSchema: z.string() ์˜ต์…˜์€ ์ผ๋ฐ˜ ํ…์ŠคํŠธ ์‘๋‹ต์„ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค:

import { node, NodeContext, RequestInput, Workflow } from '@google/adk';
import { z } from 'zod';

/** Asks the user for itinerary information. */
const initialPrompt = node(
  async function* () {
    const inputMessage = `
        This is an interactive concierge workflow tasked with making you a great
        itinerary for you in your city of choice. If you give some details about
        yourself or what you are generally looking for I can better personalize
        your itinerary.
        For example, input your:
            City (Required),
            Age,
            Hobby,
            Example of attraction you liked
    `;
    yield new RequestInput({
      message: inputMessage,
      responseSchema: z.string(),
    });
  },
  { name: 'initial_prompt' },
);

const buildItinerary = node(
  (_ctx: NodeContext, nodeInput: string) => {
    const [city = 'your city'] = nodeInput.split(',');
    return (
      `Personalized itinerary for ${city.trim()}:\n` +
      '  1. Morning walk through the old town\n' +
      '  2. Lunch at a neighbourhood favourite\n' +
      '  3. An afternoon activity matched to your hobby\n\n' +
      `(based on: ${nodeInput.trim()})`
    );
  },
  { name: 'build_itinerary' },
);

export const rootAgent = new Workflow({
  name: 'concierge_workflow',
  edges: [['START', initialPrompt, buildItinerary]],
});

Set RequireConfirmation: true in functiontool.Config for a static yes/no approval before a tool executes, or call ctx.RequestConfirmation from inside the tool for a custom hint message:

// DoubleNumberArgs holds the input for the doubleNumber tool.
type DoubleNumberArgs struct {
    Number int `json:"number" jsonschema:"The number to double."`
}

// DoubleNumberResults holds the output of the doubleNumber tool.
type DoubleNumberResults struct {
    Result int `json:"result"`
}

// doubleNumber is a tool that doubles the given number.
// Because RequireConfirmation is true, the framework automatically pauses
// execution and emits an "adk_request_confirmation" event to the client before
// running the tool. The client must reply with a FunctionResponse confirming
// or denying the action.
func doubleNumber(_ agent.Context, args DoubleNumberArgs) (DoubleNumberResults, error) {
    return DoubleNumberResults{Result: args.Number * 2}, nil
}

// newSimpleHITLAgent creates an LLM agent with a tool that always requires
// user confirmation before it executes (tool-confirmation pattern).
func newSimpleHITLAgent(ctx context.Context) (agent.Agent, error) {
    model, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{})
    if err != nil {
        return nil, fmt.Errorf("failed to create model: %w", err)
    }

    doubleNumberTool, err := functiontool.New(
        functiontool.Config{
            Name:                "double_number",
            Description:         "Doubles the given number. Requires user approval before running.",
            RequireConfirmation: true,
        },
        doubleNumber,
    )
    if err != nil {
        return nil, fmt.Errorf("failed to create tool: %w", err)
    }

    return llmagent.New(llmagent.Config{
        Name:        "double_number_agent",
        Model:       model,
        Instruction: "You are a helpful assistant. When asked to double a number, use the double_number tool.",
        Tools:       []tool.Tool{doubleNumberTool},
    })
}

For a custom hint with manual re-entry handling:

// BookFlightArgs holds the input for the bookFlight tool.
type BookFlightArgs struct {
    Origin      string `json:"origin"      jsonschema:"Departure airport code."`
    Destination string `json:"destination" jsonschema:"Arrival airport code."`
    Date        string `json:"date"        jsonschema:"Travel date in YYYY-MM-DD format."`
}

// BookFlightResults holds the outcome of the bookFlight tool.
type BookFlightResults struct {
    Status        string `json:"status"`
    ConfirmNumber string `json:"confirm_number,omitempty"`
}

// bookFlight is a tool that pauses for human approval before completing a
// booking (tool-confirmation pattern with a custom hint message).
func bookFlight(ctx agent.Context, args BookFlightArgs) (BookFlightResults, error) {
    if confirmation := ctx.ToolConfirmation(); confirmation != nil {
        if !confirmation.Confirmed {
            return BookFlightResults{Status: "Booking cancelled by user."}, nil
        }
        return BookFlightResults{
            Status:        "Booking confirmed.",
            ConfirmNumber: "FLT-20251031",
        }, nil
    }

    hint := fmt.Sprintf(
        "The agent wants to book a flight from %s to %s on %s. Do you approve?",
        args.Origin, args.Destination, args.Date,
    )
    if err := ctx.RequestConfirmation(hint, nil); err != nil {
        return BookFlightResults{}, fmt.Errorf("failed to request confirmation: %w", err)
    }
    return BookFlightResults{Status: "Awaiting user approval."}, nil
}

// newHITLWithHintAgent creates an LLM agent whose bookFlight tool manually
// requests confirmation with a descriptive hint (tool-confirmation pattern).
func newHITLWithHintAgent(ctx context.Context) (agent.Agent, error) {
    model, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{})
    if err != nil {
        return nil, fmt.Errorf("failed to create model: %w", err)
    }

    bookFlightTool, err := functiontool.New(
        functiontool.Config{
            Name:        "book_flight",
            Description: "Books a flight between two airports on a given date.",
        },
        bookFlight,
    )
    if err != nil {
        return nil, fmt.Errorf("failed to create tool: %w", err)
    }

    return llmagent.New(llmagent.Config{
        Name:        "flight_booking_agent",
        Model:       model,
        Instruction: "You are a flight booking assistant. Help the user book flights.",
        Tools:       []tool.Tool{bookFlightTool},
    })
}