Human input for agent workflows¶
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.ResumeOrRequestInputemits asession.RequestInputevent (surfaced asEvent.RequestedInput) and returnsErrNodeInterrupted, pausing the workflow. - After the human replies, the node is re-invoked from the top
(
RerunOnResume: &true) andResumeOrRequestInputreturns the reply payload, which flows as typed input to the next node viaevent.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'smessageparameter.Payload(any): Optional structured data sent alongside the prompt so the client can render additional context. Equivalent to Python'spayloadparameter.
workflow.NodeConfig.RerunOnResume controls what happens on resume:
&true: the node body is re-run from the top;ResumeOrRequestInputreturns the human's reply on the second pass. Required for nodes that useResumeOrRequestInput.&falseornil(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},
})
}