skip to content

In Google ADK, how does LongRunningFunctionTool handle a pending human approval?

level: seniorimportance: should knowfreq 42%

answer

  1. Return now, answer later
  2. The call is left open on purpose
  3. Provisional result, not a fake success
  4. The call id is the correlation key
  5. Completion arrives as a fresh message

basics

~20 s

The wrapped function returns an interim result immediately, such as a pending status with a ticket id, and ADK marks that call as long-running on the event it emits. The turn ends without blocking; the real outcome is delivered later as a function response carrying the same call id.

solid answer

~50 s

Wrap the function with `LongRunningFunctionTool(func=...)`. The function does the cheap part — open the ticket, enqueue the job — and returns straight away with an interim payload like `{"status": "pending", "ticket_id": ...}`. ADK emits the tool response event with that call's id listed in the event's long-running tool ids, so your application knows this result is not final. Nothing blocks: the agent's turn completes and the process is free. When the human eventually approves, your app resumes the conversation by sending a new message containing a function response with the *same* function call id and name, now carrying the final payload. The model sees it as the completion of the call it made earlier and continues. The alternative — a plain tool that sleeps until a human answers — holds an invocation open for hours and dies with the process.

code

python · 20 lines
python
from google.adk.tools import LongRunningFunctionTool, ToolContext

_TICKETS: dict[str, str] = {}


def request_approval(amount: float, reason: str, tool_context: ToolContext) -> dict:
    """Opens a spend-approval ticket and returns immediately.

    The returned status is provisional: approval has been requested, not granted.

    Args:
        amount: The amount to approve, in euros.
        reason: Why the spend is needed.
    """
    ticket_id = f"AP-{len(_TICKETS) + 1}"
    _TICKETS[ticket_id] = tool_context.function_call_id
    return {"status": "pending", "ticket_id": ticket_id, "amount": amount}


approval_tool = LongRunningFunctionTool(func=request_approval)

go deeper

for a junior

Know that this tool type returns straight away with a pending result instead of waiting, and that the real answer is delivered to the conversation later.

for a middle

Explain the mechanics: the wrapper, the provisional dict, the long-running marker on the event, and the later function response carrying the original call id.

for a senior

Show what you have to build around it — ticket-to-session persistence, the resumption worker, timeouts, idempotent completion, and authorization of whoever sends the final response.

for a principal

Own the policy: which actions require a human gate at all, what the approval SLA and expiry behaviour are, and how approvals are audited when the approver and the agent are different systems.

## The problem it solves Some tool calls cannot answer in a request cycle: a manager must approve a refund, a build must finish, a document must be countersigned. A plain function tool has only two bad options. It can block, holding an invocation and its resources open for however long the human takes — and losing everything if the process restarts. Or it can return "I've asked" and drop the thread, leaving the model with no way to learn the outcome except by polling with another tool call. `LongRunningFunctionTool` gives the third option: a call that is explicitly *left open* and completed later. ## The mechanics 1. **Wrap the function.** `LongRunningFunctionTool(func=request_approval)` and put the wrapper in the agent's `tools`. Nothing changes about how the model sees it — same name, same parameters, same docstring-derived description. 2. **Return fast with an interim result.** The body performs only the initiating work: create the approval ticket, publish the job, record the request. Then it returns a dict describing the *state*, not the outcome — conventionally something like `{"status": "pending", "ticket_id": "AP-8471"}`. 3. **ADK marks the call.** The event carrying that tool response records the call's id among the event's long-running tool ids. That marker is the signal to your application layer: this function response is provisional, and a final one is expected. 4. **The turn ends.** The model receives the pending result and normally tells the user that approval has been requested. No thread is parked, no invocation is held. 5. **The result arrives later.** When the human decides — minutes or days later — your application sends a new message into the same session containing a function response with the same function call id and the same tool name, carrying the real payload (`{"status": "approved", "approver": ...}`). The model treats it as the completion of the call it made earlier and continues from there. The function call id is the hinge of the whole design. It is what lets a response written by a completely different process, on a different day, be matched to the model's original request. A tool that needs to stash that id for its own bookkeeping can read it from the injected tool context. ## What the pattern buys you - **No held resources.** The gap between request and answer costs nothing — no thread, no open connection, no timeout to tune. - **Restart survivability.** Because the pending state lives in the session (and in your own ticket system), a redeploy in the middle of the wait is harmless; the completion is just a message into the session. - **A natural human-in-the-loop shape.** The user sees a truthful "approval requested" message rather than a spinner, and the conversation resumes where it left off. - **It generalises past humans.** Anything slow and external fits: a batch render, a long ETL job, an overnight fraud review. ## What you own ADK gives you the protocol, not the plumbing. You still have to build: - **The external state.** The ticket, its status, and the mapping from ticket to session and function call id. That mapping is your responsibility; ADK does not persist a queue of outstanding approvals for you. - **The resumption path.** A webhook or worker that, on approval, loads the session and sends the completing function response. - **Timeouts and cancellation.** Nothing expires on its own. Decide what happens when nobody approves in three days, and make sure the conversation says something sensible if the user asks in the meantime. - **Authorization.** The completing message is just a message. Whatever sends it must be trusted, or anyone who can post into a session can approve their own refund. Verify the approver in *your* system before emitting the completion. - **Idempotency.** Retries and duplicate webhooks happen; a second completion for the same call id should be a no-op. ## Failure modes seen in practice The most common is doing real work inside the function anyway — awaiting the approval, polling in a loop — which reintroduces exactly the blocking the tool exists to avoid. The second is returning a plausible-looking success in the interim payload: the model then tells the user the refund was approved before any human looked at it. Make the interim status unmistakably provisional and say so in the docstring so the model phrases it correctly. The third is losing the id, which strands the call permanently — the model never learns the outcome and, on the next turn, may simply call the tool again. ## What interviewers listen for That the tool returns immediately with provisional state; that the marker plus the function call id are what make later completion possible; that the process never blocks; and that everything around the gap — persistence, authorization, timeouts, idempotency — is application work the framework does not do for you.

  • What stops a malicious caller from sending the completing function response themselves?
    Nothing in the framework — the completion is an ordinary message into the session, so authorization is yours to enforce. The approval decision must be verified in your own system (who approved, against which ticket, with what rights) before the worker or webhook is allowed to emit the completion. Treat the ability to post into a session as equivalent to the ability to approve, and gate it accordingly.
  • How do you handle an approval that never comes?
    Build the expiry yourself; nothing times out on its own. A scheduled job scans open tickets and, past the deadline, sends the completion with a rejected or expired payload so the conversation resolves instead of dangling. Also make the interim message honest about the wait, and make repeated user prompts read the ticket state rather than triggering a second approval request.
  • Why not just have the tool poll until the human responds?
    Polling reintroduces the blocking the design removes: an invocation stays alive for the whole wait, consuming resources and dying on any redeploy, and you inherit timeout tuning at every layer between the client and the model. The long-running pattern moves the wait outside the request entirely, so the gap costs nothing and a restart is harmless.

Raising a ticket rather than waiting on hold. You get a reference number immediately, hang up, and the answer comes back later quoting that number.

saying these in an interview costs you the question

  • Thinks the tool function waits for the human to respond
  • Returns a success payload before anyone has approved
  • Loses the function call id so the result can never be delivered
  • Assumes ADK persists and retries outstanding approvals for you
  • Believes the completion needs no authorization because it is internal

context