All skills
google avatar

/adk-agent-builder

@29933ce
by googlegoogle/adk-python22k stars
4,084

Builds ADK (Agent Development Kit) Python agents: LLM agents with tools, graph workflows of function and agent nodes, conditional routing, fan-out and join, schema-validated delegation between agents, human-in-the-loop pauses, and pytest coverage for all of it. Use when asked to create an agent or a workflow, add a tool to one, branch or loop between nodes, run steps in parallel, pause for user approval, or test an agent. Don't use for explaining how ADK works internally or designing its core components (use `adk-architecture`), for an agent that already runs but misbehaves (use `adk-debug`), for authoring a sample under `contributing/` (use `adk-sample-creator`), or for naming, typing, and formatting conventions (use `adk-style`).

Use this Skill: https://skilld.dev/gh/google/adk-python/adk-agent-builder

This session only. Nothing lands on disk.

referenceshuman-in-the-loop.md

≈1.5k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Human-in-the-Loop

Pause a workflow to ask the user something, then continue with their answer.

from google.adk import Context, Event, Workflow
from google.adk.apps import App, ResumabilityConfig
from google.adk.events import RequestInput
from google.adk.workflow import FunctionNode

Asking

Yield or return a RequestInput. The node's output stream is normalized, so either works — a plain function can return one directly without becoming a generator.

async def approval_gate(ctx: Context, node_input: str):
  yield RequestInput(message='Please approve this action:')


def evaluate_request(request: TimeOffRequest):
  if request.days <= 1:
    return TimeOffDecision(approved=True)  # no interrupt at all
  return RequestInput(
      interrupt_id='manager_approval',
      message='Please review this time off request.',
      payload=request,
      response_schema=TimeOffDecision,
  )

The workflow emits an adk_request_input function call and stops. Responding to that function call resumes it.

Field Type Notes
interrupt_id str Auto-generated UUID when omitted
message str | None Shown to the user
payload Any Arbitrary data carried along with the request
response_schema Pydantic class, Python type, or JSON-schema dict Expected response shape

Resuming: rerun_on_resume

The flag on the interrupted node decides what happens when the answer arrives.

rerun_on_resume=False (the default for FunctionNode) — the node is not re-executed; the user's response becomes its output and flows downstream.

approval_node = FunctionNode(func=ask_approval, rerun_on_resume=False)

rerun_on_resume=True (the default for an LlmAgent used as a node) — the node runs again from the top, with answers available in ctx.resume_inputs, keyed by interrupt_id.

async def interactive_node(ctx: Context, node_input: str):
  if ctx.resume_inputs:
    answer = list(ctx.resume_inputs.values())[0]
    yield Event(output=f'User said: {answer}')
  else:
    yield RequestInput(message='What should I do?')

Several questions from one node

Because a re-run node sees every answer so far, one node can walk a form:

async def multi_step_form(ctx: Context, node_input: str):
  if not ctx.resume_inputs:
    yield RequestInput(interrupt_id='ask_name', message='What is your name?')
    return

  if 'ask_email' not in ctx.resume_inputs:
    yield RequestInput(interrupt_id='ask_email', message='What is your email?')
    return

  yield Event(output={
      'name': ctx.resume_inputs['ask_name'],
      'email': ctx.resume_inputs['ask_email'],
  })

Feeding the answer back into a loop

A review-and-revise cycle turns the answer into a route. Vary the interrupt_id per iteration (see the best-practices reference for why) and read ctx.resume_inputs with the same id:

async def review(ctx: Context, node_input: Any):
  review_count = ctx.state.get('review_count', 0)
  interrupt_id = f'review_{review_count}'

  response = ctx.resume_inputs.get(interrupt_id)
  if response:
    yield Event(
        output=response,
        route='approved' if response.get('approved') else 'rejected',
        state={'review_count': review_count + 1},
    )
    return

  yield RequestInput(
      interrupt_id=interrupt_id,
      message='Approve this plan?',
      response_schema=ApprovalSchema,
  )

Wire the routes back with (review, {'rejected': revise, 'approved': publish}).

Resumable versus replayed

Replay (the default). With no App, or with is_resumable=False, each response replays the workflow from START. Completed nodes are skipped and state is rebuilt from event history, so only the interrupted node actually runs again. Fine for a single interrupt; the replay cost grows with the graph.

Resumable. Export an App with is_resumable=True and the workflow checkpoints its progress into event.actions.agent_state and resumes at the interrupted node instead of replaying.

root_agent = Workflow(name='my_workflow', edges=[...])

app = App(
    name='my_app',
    root_agent=root_agent,
    resumability_config=ResumabilityConfig(is_resumable=True),
)

Export both root_agent and app from agent.py — the loader prefers app but other tooling looks for root_agent. Use resumable mode for multi-step interrupts, for LongRunningFunctionTool, and for any graph large enough that replaying it is wasteful.

Human-in-the-loop from an LLM agent

An LlmAgent pauses through a LongRunningFunctionTool rather than RequestInput:

from google.adk.tools import LongRunningFunctionTool


def approval_tool(request: str) -> str:
  """Request human approval for an action."""
  return f'Approved: {request}'


llm_agent = LlmAgent(
    name='agent_with_approval',
    model='gemini-2.5-flash',
    instruction='When you need approval, use approval_tool.',
    tools=[LongRunningFunctionTool(func=approval_tool)],
)

The agent node already defaults to rerun_on_resume=True, so it picks the conversation back up on its own.

Answering from client code

from google.genai import types

function_call_id = interrupt_event.content.parts[0].function_call.id

response = types.Content(
    role='user',
    parts=[types.Part(
        function_response=types.FunctionResponse(
            id=function_call_id,
            name='adk_request_input',
            response={'result': "User's answer here"},
        )
    )],
)

async for event in runner.run_async(
    user_id=user_id, session_id=session_id, new_message=response
):
  ...

A non-dict answer is carried under the result key as shown; a dict response matching response_schema is passed through as-is.

Source: SKILL.md on GitHub

No alerts7d3 checks · Risk SAFE
  • Gen Agent Trust Hub7d

    This skill provides comprehensive documentation and reference material for building agents using the Google Agent Development Kit (ADK). It covers workflow orchestration, tool usage, and includes guidance on security best practices like input validation and secure credential management. No security issues were detected.

  • Socket7d

    No alerts

  • Snyk7d

    Risk: LOW · No issues

Signed by skilld at 29933ce. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated 2 months ago

README badge

README badge for google/adk-python/adk-agent-builder