Tool confirmations, auth, input requests, artifacts, escalation, metadata, structured events.
Reference for the runtime's React hooks. These expose ADK-specific state and HITL primitives. Start with quickstart if you have not wired up a basic runtime yet.
Agent and session state
import {
useAdkAgentInfo,
useAdkSessionState,
useAdkSend,
} from "@assistant-ui/react-google-adk";
function MyComponent() {
// Current active agent name and branch path (multi-agent)
const agentInfo = useAdkAgentInfo();
// agentInfo?.name = "search_agent"
// agentInfo?.branch = "root.search_agent"
// Accumulated session state delta
const state = useAdkSessionState();
// Send ADK messages programmatically
const send = useAdkSend();
}Tool confirmations
When ADK's SecurityPlugin or tool callbacks request user confirmation before executing a tool, use useAdkToolConfirmations to read pending requests and useAdkConfirmTool to respond:
import {
useAdkToolConfirmations,
useAdkConfirmTool,
} from "@assistant-ui/react-google-adk";
function ToolConfirmationUI() {
const confirmations = useAdkToolConfirmations();
const confirmTool = useAdkConfirmTool();
if (confirmations.length === 0) return null;
return confirmations.map((conf) => (
<div key={conf.toolCallId}>
<p>
Tool "{conf.toolName}" wants to run. {conf.hint}
</p>
<button onClick={() => confirmTool(conf.toolCallId, true)}>Approve</button>
<button onClick={() => confirmTool(conf.toolCallId, false)}>Deny</button>
</div>
));
}import {
useAdkToolConfirmations,
useAdkConfirmTool,
} from "@assistant-ui/react-google-adk";
import { Pressable, Text, View } from "react-native";
function ToolConfirmationUI() {
const confirmations = useAdkToolConfirmations();
const confirmTool = useAdkConfirmTool();
if (confirmations.length === 0) return null;
return (
<View>
{confirmations.map((conf) => (
<View key={conf.toolCallId}>
<Text>
Tool "{conf.toolName}" wants to run. {conf.hint}
</Text>
<Pressable onPress={() => confirmTool(conf.toolCallId, true)}>
<Text>Approve</Text>
</Pressable>
<Pressable onPress={() => confirmTool(conf.toolCallId, false)}>
<Text>Deny</Text>
</Pressable>
</View>
))}
</View>
);
}import {
useAdkToolConfirmations,
useAdkConfirmTool,
} from "@assistant-ui/react-google-adk";
import { Box, Text, useInput } from "ink";
function ToolConfirmationUI() {
const confirmations = useAdkToolConfirmations();
const confirmTool = useAdkConfirmTool();
const active = confirmations[0];
useInput((input) => {
if (!active) return;
if (input === "y") confirmTool(active.toolCallId, true);
if (input === "n") confirmTool(active.toolCallId, false);
});
if (!active) return null;
return (
<Box flexDirection="column">
<Text>
Tool "{active.toolName}" wants to run. {active.hint}
</Text>
<Text>Press y to approve, n to deny.</Text>
</Box>
);
}Auth requests
When a tool requires OAuth or other authentication, ADK pauses it and emits an adk_request_credential call carrying the auth config it issued. useAdkAuthRequests lists the calls still waiting, and useAdkSubmitAuth answers one with a credential. The reply is that request's authConfig with the credential as its exchangedAuthCredential, which is the shape ADK resumes the tool on, so the id has to be one the list currently holds; a stale one throws instead of sending a reply ADK would ignore.
For OAuth2 and OpenID Connect the app owns the browser round trip: open authConfig.exchangedAuthCredential.oauth2.authUri, then submit the URL ADK redirected the user back to as oauth2.authResponseUri (or the code alone as oauth2.authCode). ADK exchanges the code for tokens itself. adk-js reads nothing else off an OAuth2 reply; adk-python also accepts a credential that already carries oauth2.accessToken and stores it without an exchange. The examples call an app-owned completeOAuth(authUri) that resolves with that redirect URL.
import {
useAdkAuthRequests,
useAdkSubmitAuth,
type AdkAuthCredential,
} from "@assistant-ui/react-google-adk";
const authUriOf = (authConfig: unknown) =>
(authConfig as { exchangedAuthCredential?: AdkAuthCredential } | undefined)
?.exchangedAuthCredential?.oauth2?.authUri;
function AuthUI() {
const authRequests = useAdkAuthRequests();
const submitAuth = useAdkSubmitAuth();
if (authRequests.length === 0) return null;
return authRequests.map((req) => (
<div key={req.toolCallId}>
<button
onClick={async () => {
const authResponseUri = await completeOAuth(authUriOf(req.authConfig));
submitAuth(req.toolCallId, {
authType: "oauth2",
oauth2: { authResponseUri },
});
}}
>
Sign in
</button>
</div>
));
}import {
useAdkAuthRequests,
useAdkSubmitAuth,
type AdkAuthCredential,
} from "@assistant-ui/react-google-adk";
import { Pressable, Text, View } from "react-native";
const authUriOf = (authConfig: unknown) =>
(authConfig as { exchangedAuthCredential?: AdkAuthCredential } | undefined)
?.exchangedAuthCredential?.oauth2?.authUri;
function AuthUI() {
const authRequests = useAdkAuthRequests();
const submitAuth = useAdkSubmitAuth();
if (authRequests.length === 0) return null;
return (
<View>
{authRequests.map((req) => (
<Pressable
key={req.toolCallId}
onPress={async () => {
const authResponseUri = await completeOAuth(
authUriOf(req.authConfig),
);
submitAuth(req.toolCallId, {
authType: "oauth2",
oauth2: { authResponseUri },
});
}}
>
<Text>Sign in</Text>
</Pressable>
))}
</View>
);
}import {
useAdkAuthRequests,
useAdkSubmitAuth,
type AdkAuthCredential,
} from "@assistant-ui/react-google-adk";
import { Box, Text, useInput } from "ink";
const authUriOf = (authConfig: unknown) =>
(authConfig as { exchangedAuthCredential?: AdkAuthCredential } | undefined)
?.exchangedAuthCredential?.oauth2?.authUri;
function AuthUI() {
const authRequests = useAdkAuthRequests();
const submitAuth = useAdkSubmitAuth();
const active = authRequests[0];
useInput(async (input) => {
if (!active || input !== "a") return;
const authResponseUri = await completeOAuth(authUriOf(active.authConfig));
submitAuth(active.toolCallId, {
authType: "oauth2",
oauth2: { authResponseUri },
});
});
if (!active) return null;
return (
<Box flexDirection="column">
<Text>Authentication required for a tool call.</Text>
<Text>Press a to sign in.</Text>
</Box>
);
}AdkAuthCredential supports all ADK auth types: apiKey, http, oauth2, openIdConnect, serviceAccount. A scheme without a browser round trip takes the credential directly, such as submitAuth(req.toolCallId, { authType: "apiKey", apiKey }).
Input requests
When an ADK Python 2.0+ Workflow's RequestInput node pauses execution to ask the user a question, ADK emits an adk_request_input function call marked as long-running. Respond with useAdkSubmitInput inside a tool UI; the helper wraps the answer as { result } to match ADK's unwrap_response contract, so the Workflow node resumes with the unwrapped value:
import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
import { useAdkSubmitInput } from "@assistant-ui/react-google-adk";
type RequestInputArgs = {
interrupt_id?: string;
message?: string;
payload?: unknown;
response_schema?: unknown;
};
export const RequestInputToolUI: ToolCallMessagePartComponent<
RequestInputArgs,
unknown
> = function RequestInputUI({ toolCallId, args, result }) {
const submitInput = useAdkSubmitInput();
if (result !== undefined) {
return <p>Answered: {String(result)}</p>;
}
return (
<form
onSubmit={(e) => {
e.preventDefault();
const value = (
e.currentTarget.elements.namedItem("answer") as HTMLInputElement
).value;
submitInput(toolCallId, value);
}}
>
<p>{args.message ?? "Please provide input:"}</p>
<input name="answer" autoFocus />
<button type="submit">Submit</button>
</form>
);
};import type { ToolCallMessagePartComponent } from "@assistant-ui/react-native";
import { useAdkSubmitInput } from "@assistant-ui/react-google-adk";
import { useState } from "react";
import { Pressable, Text, TextInput, View } from "react-native";
type RequestInputArgs = {
interrupt_id?: string;
message?: string;
payload?: unknown;
response_schema?: unknown;
};
export const RequestInputToolUI: ToolCallMessagePartComponent<
RequestInputArgs,
unknown
> = function RequestInputUI({ toolCallId, args, result }) {
const submitInput = useAdkSubmitInput();
const [value, setValue] = useState("");
if (result !== undefined) {
return <Text>Answered: {String(result)}</Text>;
}
return (
<View>
<Text>{args.message ?? "Please provide input:"}</Text>
<TextInput value={value} onChangeText={setValue} autoFocus />
<Pressable onPress={() => submitInput(toolCallId, value)}>
<Text>Submit</Text>
</Pressable>
</View>
);
};import type { ToolCallMessagePartComponent } from "@assistant-ui/react-ink";
import { useAdkSubmitInput } from "@assistant-ui/react-google-adk";
import { useState } from "react";
import { Box, Text, useInput } from "ink";
type RequestInputArgs = {
interrupt_id?: string;
message?: string;
payload?: unknown;
response_schema?: unknown;
};
export const RequestInputToolUI: ToolCallMessagePartComponent<
RequestInputArgs,
unknown
> = function RequestInputUI({ toolCallId, args, result }) {
const submitInput = useAdkSubmitInput();
const [value, setValue] = useState("");
useInput((input, key) => {
if (result !== undefined) return;
if (key.return) submitInput(toolCallId, value);
else if (key.backspace || key.delete) setValue((text) => text.slice(0, -1));
else if (input) setValue((text) => text + input);
});
if (result !== undefined) {
return <Text>Answered: {String(result)}</Text>;
}
return (
<Box flexDirection="column">
<Text>{args.message ?? "Please provide input:"}</Text>
<Text>{value}</Text>
<Text dimColor>Press enter to submit.</Text>
</Box>
);
};Register the renderer with a backend toolkit entry inside AssistantRuntimeProvider:
import {
AssistantRuntimeProvider,
AuiConfig,
defineToolkit,
Tools,
} from "@assistant-ui/react";
import {
createAdkStream,
useAdkRuntime,
} from "@assistant-ui/react-google-adk";
import { Thread } from "@/components/assistant-ui/elements/thread.aui";
const toolkit = defineToolkit({
adk_request_input: {
type: "backend",
render: RequestInputToolUI,
},
});
function App() {
const runtime = useAdkRuntime({
stream: createAdkStream({ api: "/api/chat" }),
});
const config = AuiConfig({ tools: Tools({ toolkit }) });
return (
<AssistantRuntimeProvider runtime={runtime} config={config}>
<Thread />
</AssistantRuntimeProvider>
);
}import {
AssistantRuntimeProvider,
AuiConfig,
defineToolkit,
Tools,
} from "@assistant-ui/react-native";
import {
createAdkStream,
useAdkRuntime,
} from "@assistant-ui/react-google-adk";
import { View } from "react-native";
import { Thread } from "@/components/assistant-ui/elements/thread.aui";
const API_URL = process.env.EXPO_PUBLIC_API_URL ?? "http://localhost:3000";
const toolkit = defineToolkit({
adk_request_input: {
type: "backend",
render: RequestInputToolUI,
},
});
function App() {
const runtime = useAdkRuntime({
stream: createAdkStream({ api: `${API_URL}/api/chat` }),
});
const config = AuiConfig({ tools: Tools({ toolkit }) });
return (
<AssistantRuntimeProvider runtime={runtime} config={config}>
<View style={{ flex: 1 }}>
<Thread />
</View>
</AssistantRuntimeProvider>
);
}import {
AssistantRuntimeProvider,
AuiConfig,
defineToolkit,
Tools,
} from "@assistant-ui/react-ink";
import {
createAdkStream,
useAdkRuntime,
} from "@assistant-ui/react-google-adk";
import { Box } from "ink";
import { Thread } from "./components/thread.js";
const toolkit = defineToolkit({
adk_request_input: {
type: "backend",
render: RequestInputToolUI,
},
});
function App() {
const runtime = useAdkRuntime({
stream: createAdkStream({ api: "http://localhost:3000/api/chat" }),
});
const config = AuiConfig({ tools: Tools({ toolkit }) });
return (
<AssistantRuntimeProvider runtime={runtime} config={config}>
<Box flexDirection="column">
<Thread />
</Box>
</AssistantRuntimeProvider>
);
}Info
adk_request_input is emitted only by ADK Python 2.0+ Workflow RequestInput nodes; ADK JS has no equivalent. Always respond via a tool UI with useAdkSubmitInput. HITL interrupts are automatically exempt from autoCancelPendingToolCalls, so typing a normal message in the composer will not overwrite the pending interrupt.
useAdkSubmitInput is sugar over the generic addResult. If you prefer, call addResult({ result: value }) from inside the render function directly. The { result } wrapper is required either way: the adapter JSON-stringifies the value before sending, and ADK's unwrap_response unwraps it on the backend before the Workflow node resumes.
Artifacts
Track file artifacts created or modified by the agent:
import { useAdkArtifacts } from "@assistant-ui/react-google-adk";
function ArtifactList() {
const artifacts = useAdkArtifacts();
// Record<string, number> — filename to version number
}Artifact fetching
When using createAdkSessionAdapter (see api reference), the returned artifacts object provides functions to fetch artifact content from the ADK server:
const { artifacts } = createAdkSessionAdapter({ apiUrl, appName, userId });
// List all artifact filenames in a session
const filenames = await artifacts.list(sessionId);
// Load artifact content (latest version)
const data = await artifacts.load(sessionId, "document.pdf");
// data.fileData?.fileUri — external artifact URI
// data.fileData?.mimeType — MIME type (if provided)
// data.inlineData?.data — base64 content
// data.inlineData?.mimeType — MIME type
// data.text — text content (if text artifact)
// Load a specific version
const v1 = await artifacts.load(sessionId, "document.pdf", 1);
// List all versions
const versions = await artifacts.listVersions(sessionId, "document.pdf");
// Delete an artifact
await artifacts.delete(sessionId, "document.pdf");Escalation
Detect when an agent requests escalation to a human operator:
import { useAdkEscalation } from "@assistant-ui/react-google-adk";
function EscalationBanner() {
const escalated = useAdkEscalation();
if (!escalated) return null;
return <div>Agent has requested human assistance.</div>;
}import { useAdkEscalation } from "@assistant-ui/react-google-adk";
import { Text } from "react-native";
function EscalationBanner() {
const escalated = useAdkEscalation();
if (!escalated) return null;
return <Text>Agent has requested human assistance.</Text>;
}import { useAdkEscalation } from "@assistant-ui/react-google-adk";
import { Text } from "ink";
function EscalationBanner() {
const escalated = useAdkEscalation();
if (!escalated) return null;
return <Text>Agent has requested human assistance.</Text>;
}Long-running tools
Track tools that are executing asynchronously and awaiting external input:
import { useAdkLongRunningToolIds } from "@assistant-ui/react-google-adk";
function PendingToolsIndicator() {
const pendingToolIds = useAdkLongRunningToolIds();
if (pendingToolIds.length === 0) return null;
return <div>{pendingToolIds.length} tool(s) awaiting input</div>;
}import { useAdkLongRunningToolIds } from "@assistant-ui/react-google-adk";
import { Text } from "react-native";
function PendingToolsIndicator() {
const pendingToolIds = useAdkLongRunningToolIds();
if (pendingToolIds.length === 0) return null;
return <Text>{pendingToolIds.length} tool(s) awaiting input</Text>;
}import { useAdkLongRunningToolIds } from "@assistant-ui/react-google-adk";
import { Text } from "ink";
function PendingToolsIndicator() {
const pendingToolIds = useAdkLongRunningToolIds();
if (pendingToolIds.length === 0) return null;
return <Text>{pendingToolIds.length} tool(s) awaiting input</Text>;
}This hook reports every tool call ADK marked via long_running_tool_ids, including HITL interrupts, until the client sends its first response for that call. That first response ends the report even when it is only a progress update, because nothing in the event tells a progress update apart from a final response. A value the long-running tool itself returns reaches the thread as an interim result and leaves the call pending. To respond to a specific HITL type, see tool confirmations, auth requests, or input requests.
That same set decides which tool calls the client owns. A registered frontend tool's execute runs only for a call ADK marked long-running, because ADK resolves every other call itself and yields the call to the client before its own response. Register a tool the client executes as a LongRunningFunctionTool on the agent.
The client answers the long-running calls of one run together. Answers from addToolResult, approval replies, and the HITL helpers show in the thread at once but wait while any run is active. Once no run is active, held replies and replies given in the same tick go out one producing run per request. For calls loaded with the thread, one assistant message takes the place of a producing run. Client-owned long-running calls from the same run wait until all have answers, then go out in the order the calls appear in the thread. An interrupt answer does not wait for other calls. A held answer remains in useAdkLongRunningToolIds until it is sent, but a held confirmation or auth request disappears from its pending request hook as soon as it is buffered. A held reply's promise resolves when it is buffered, as with addToolResult. While a reply is held locally, its tool-call part has responseDeliveryStatus: "queued"; the field disappears when the reply is sent or dropped. When a run ends in an error, the release that follows it holds back that run's own answers; they go out once another run finishes, or with the next user message, ahead of it. A held answer stays with its thread when you switch away and back. Cancelling the run or reloading the thread drops held answers. A message edit or regenerate that fails before its first event restores held answers with the rest of the thread, even when it was made while an earlier edit or regenerate was pending. Stop interrupting a checkpoint lookup (with unstable_allowCancellation) also restores them. They go out once another run finishes or with the next user message.
A long-running call is not background work. @assistant-ui/core keeps a message running after its run ends only while a tool call carries a nested conversation whose trailing assistant message is still running; an ADK long-running call carries no nested conversation, and the run that yielded it has ended. It normally settles on requires-action with reason tool-calls until the function response is sent; an unresolved confirmation approval instead uses reason interrupt. Either state renders the tool UI and keeps useAdkSubmitInput, useAdkConfirmTool, and useAdkSubmitAuth answerable. When a frontend tool is registered for the call, its execute puts the thread back into isRunning for as long as it runs.
Per-message metadata
Access grounding, citation, and token usage metadata per message:
import { useAdkMessageMetadata } from "@assistant-ui/react-google-adk";
function MessageMetadata({ messageId }: { messageId: string }) {
const metadataMap = useAdkMessageMetadata();
const meta = metadataMap.get(messageId);
// meta?.groundingMetadata — Google Search grounding sources
// meta?.citationMetadata — citation references
// meta?.usageMetadata — token counts
}Session state by scope
ADK uses key prefixes to scope state. These helpers filter and strip the prefix:
import {
useAdkAppState,
useAdkUserState,
useAdkTempState,
} from "@assistant-ui/react-google-adk";
function StateDebug() {
const appState = useAdkAppState(); // app:* keys (app-level, shared)
const userState = useAdkUserState(); // user:* keys (user-level)
const tempState = useAdkTempState(); // temp:* keys (not persisted)
}Use useAdkSessionState() for the full unfiltered state delta.
Structured events
Convert raw ADK events into typed, structured events for custom renderers:
import {
toAdkStructuredEvents,
AdkEventType,
type AdkStructuredEvent,
} from "@assistant-ui/react-google-adk";
const structured = toAdkStructuredEvents(event);
for (const e of structured) {
switch (e.type) {
case AdkEventType.CONTENT:
console.log("Text:", e.content);
break;
case AdkEventType.THOUGHT:
console.log("Reasoning:", e.content);
break;
case AdkEventType.TOOL_CALL:
console.log("Tool:", e.call.name, e.call.args);
break;
case AdkEventType.ERROR:
console.error(e.errorMessage);
break;
}
}Hooks reference
| Hook | Description |
|---|---|
useAdkAgentInfo() | Current agent name and branch path. |
useAdkSessionState() | Full accumulated session state delta. |
useAdkAppState() | App-level state (app:* prefix, stripped). |
useAdkUserState() | User-level state (user:* prefix, stripped). |
useAdkTempState() | Temp state (temp:* prefix, stripped, not persisted). |
useAdkSend() | Send ADK messages. A single interrupt tool reply with a string call ID and empty config (including { runConfig: undefined }) is held like the HITL helpers while any run is active; other shapes send immediately. |
useAdkConfirmTool() | Confirm or deny a pending tool confirmation. |
useAdkSubmitAuth() | Submit auth credentials for a pending auth request. |
useAdkSubmitInput() | Submit the user's answer for a pending adk_request_input HITL interrupt. |
useAdkToolConfirmations() | Pending tool confirmation requests. |
useAdkAuthRequests() | Pending auth credential requests. |
useAdkLongRunningToolIds() | IDs of long-running tools awaiting input. |
useAdkArtifacts() | Artifact delta (filename → version). |
useAdkEscalation() | Whether escalation was requested. |
useAdkMessageMetadata() | Per-message grounding, citation, and usage metadata. |
Feature support
| Feature | Status |
|---|---|
| Streaming text (SSE) | Supported |
| Tool calls and results | Supported |
Tool confirmations (useAdkConfirmTool) | Supported |
Auth credential flow (useAdkSubmitAuth) | Supported |
Workflow input requests (useAdkSubmitInput, ADK Python 2.0+) | Supported |
| Multi-agent (author and branch tracking) | Supported |
| Agent transfer events | Supported |
| Escalation detection | Supported |
| Chain-of-thought / reasoning | Supported |
| Code execution (executableCode + result) | Supported |
| Inline images and file data | Supported |
| Session state delta + scoped state | Supported |
| Artifact delta tracking + fetching | Supported |
| Long-running tools (HITL) | Supported |
| Grounding / citation / usage metadata | Supported |
Structured events (toAdkStructuredEvents) | Supported |
Typed AdkRunConfig | Supported |
Client → server stateDelta | Supported |
finishReason mapping | Supported |
interrupted event handling | Supported |
| Snake_case field normalization | Supported |
| Cloud thread persistence | Supported |
| ADK session-backed thread persistence | Supported |
| Direct ADK server connection (no proxy) | Supported |
One-liner API route (createAdkApiRoute) | Supported |
| Message editing and regeneration | Supported |
| Automatic tool invocations | Supported |