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},
})
}