Skip to main content

Agent

The Agent module enables your application to communicate with an AI Agent built in the ART Agent Builder. It allows you to send prompts, receive strongly typed responses as a stream of events, and respond to the agent whenever it requests input during task execution.

Prerequisites

  • The ADK is installed, authenticated, and connected. See Installation if you haven't set this up yet.
  • An agent has been created and deployed using the Agent Builder, and you have its agentId.

Workflow

Each agent communicates over its own dedicated channel. The ADK manages this channel internally and exposes three core classes, allowing you to work with strongly typed Dart objects instead of raw network frames.

ClassDescription
AgentRepresents a single AI agent identified by its agentId. Reuse the same Agent instance for all conversations with that agent.
AgentThreadRepresents a single conversation with an agent. Each thread has a unique threadId that is automatically included with every request, enabling the server to maintain conversation state.
RunRepresents a single prompt–response cycle within a thread. It manages the lifecycle of a request and provides access to the agent's final response when the run completes.

Quick start

The following example shows the basic workflow for sending a prompt to an agent and handling the response:

// 1. Get the agent and start a conversation.
val agent: Agent = adk.agent("your-agent-id")

// 2. Start a thread
val thread: AgentThread = agent.thread()

// 3. Run a prompt and wait for the final response.
val run: Run = thread.run("Plan a 3-day trip to Goa")
val result: AgentOutput = run.done()
println(result.message)

// 4. Listen for agent events
thread.listen { envelope: AgentEventEnvelope ->
when (envelope.event) {
"agent_general_response" -> {
val output = envelope.content as AgentOutput
println("Answer: ${output.message}")
}
"human_input_request" -> {
val request = envelope.content as HumanInputRequest
println("Agent needs input: ${request.prompt}")
}
// ...other cases
else -> {
val unknown = envelope.content as UnknownAgentEvent
println("Unknown event ${unknown.event}: ${unknown.content}")
}
}
}

This example illustrates the complete interaction workflow with an agent. The following sections describe each step in detail, including event processing, handling human-input requests, and managing errors.

1. Connect to an agent

Get an agent instance from the connected Adk instance. The channel subscription is established lazily — it is established automatically the first time the agent is used, and reused after that:

Agent agent = adk.agent('your-agent-id');

2. Start a thread

An AgentThread represents a single conversation with an agent. Every message sent through the thread automatically includes its unique threadId, enabling the server to maintain conversation context and associate responses with the correct thread.

AgentThread thread = agent.thread();

Log.d(thread.threadId);

3. Run a prompt

Call thread.run(...) to send a prompt to the agent. The method returns a Run, which represents the lifecycle of a single prompt–response interaction. A run begins when the prompt is submitted, progresses as the agent processes the request, and completes when the agent returns its final response.

Run run = thread.run('Plan a 3-day trip to Goa');

val AgentOutput result = run.done();
Log.d(result.message);

The following sequence diagram shows the complete lifecycle of a run when the agent does not request human input.

4. Listen for agent events

While run.done() provides the final AgentOutput for a run, many applications also need to observe intermediate events, such as progress updates or requests for additional user input. To receive these events, register a listener using thread.listen().

// listen() is suspend -> call it from a coroutine scope.
lifecycleScope.launch {
thread.listen { envelope ->
when (envelope.event) {
"agent_general_response" -> {
val output = envelope.content as AgentOutput
Log.d("Agent", "Agent: ${output.message}")
}
"agent_error_response" -> {
val error = envelope.content as AgentError
Log.e("Agent", "Error [${error.code}]: ${error.message}")
}
"human_input_request" -> {
val request = envelope.content as HumanInputRequest
Log.d("Agent", "Agent asks: ${request.prompt}")
}
"agent_wait_response" -> {
val wait = envelope.content as AgentWait
Log.d("Agent", "Waiting on agent: ${wait.waitingForAgentId}")
}
"planner_correction_request" -> {
val correction = envelope.content as PlannerCorrection
Log.d("Agent", "Planner correction: ${correction.reason}")
}
else -> {
// Anything the SDK does not model arrives as UnknownAgentEvent.
val unknown = envelope.content as UnknownAgentEvent
Log.d("Agent", "Unknown event ${unknown.event}: ${unknown.content}")
}
}
}
}

The listener callback receives an AgentEvent for every inbound event.

Event catalog

Every event payload implements EnvelopeMeta, which provides the following routing metadata:

  • threadId — The conversation to which the event belongs.
  • refId — The unique identifier of this event.
  • agentId — The identifier of the agent that emitted the event.
  • replyTo — The refId of the event to which this event is responding.

These fields may contain empty strings if the server does not populate them.

EventBodyEnds the run?Key fields
agent_general_responseAgentOutputYesrun.done() completes successfully.message, data, metadata
agent_error_responseAgentErrorYesrun.done() throws an AgentError.code, message, details
human_input_requestHumanInputRequestNo — the run waits for user input.prompt, expectedResponseType, timeout, context, schema
agent_wait_responseAgentWaitNo — progress notification only.waitingForAgentId, reason, progress, timeout
planner_correction_requestPlannerCorrectionNo — progress notification only.correctionRequired, reason, newGoal, suggestedAgents

The listener callback receives an AgentEventEnvelope for every inbound event.

MemberTypeDescription
eventStringThe event name. Use this value to determine the event type and dispatch the corresponding handler.
contentObjectThe typed event payload. Cast it to the appropriate class based on the value of event.
isKnownboolIndicates whether the event is one of the event types recognized by the ADK.

The run lifecycle

A Run progresses through a small set of well-defined states during its lifetime. Understanding these states clarifies when run.done() completes successfully with the final AgentOutput, and when it throws an exception.

Human-input-request

Some tasks require additional input from the user while they are being processed — for example, a budget, a travel date, or a confirmation. When this happens, the agent emits a human_input_request event, and the Run waits until your application provides the requested input. This interaction pattern is known as Human-in-the-Loop.

Register a handler once, using feedbackRequest(). Whenever the agent requires additional user input, it emits a human_input_request event and invokes the registered handler. Respond by calling run.sendFeedback(...) with the requested input to allow the same Run to continue.

thread.feedbackRequest { request, run ->
Log.d("Agent", "Agent asks: ${request.prompt}")

// Collect the answer from your UI, then resume the paused run:
run.sendFeedback("Under 50,000, departing December 20th")
}

Handler parameters

ParameterTypeDescription
requestHumanInputRequestContains the user prompt and its associated metadata.
runRunThe active Run waiting for user input.

HumanInputRequest fields

FieldTypeDescription
promptStringThe prompt or question to present to the user.
expectedResponseTypeExpectedResponseTypeThe expected format of the user's response.
expectedResponseTypeRawStringThe original response type received from the server, preserved for forward compatibility.
contextMap<String, dynamic>?Optional contextual information, such as rendering hints or additional state.
timeoutnum?The optional time limit, in seconds, for providing a response.
schemadynamicAn optional JSON Schema describing the expected response when expectedResponseType is structured.

expectedResponseType can be one of the following values:

ValueExpected response
textFree-form text (default).
choiceOne of several predefined options.
confirmA yes/no confirmation.
fileA file upload.
structuredA response that conforms to request.schema.

Key points

  • Call run.sendFeedback(...) only when input is pending. It responds to the most recent human_input_request. If no request is pending, the method throws a StateError. In long-lived UI code, guard against completed runs by checking if (!run.isClosed()) before sending feedback.
  • Handle request timeouts. If the request specifies a timeout (in seconds) and no response is received before it expires, the Run fails with an AgentError whose code is HUMAN_INPUT_TIMEOUT.

Reply to a specific earlier event

Every inbound event carries its own refId. To send a message as a reply to that specific event, pass it through RunDeps.replyId — the ADK sends a user_reply (instead of user_input) carrying reply_id:

AgentThread run = thread.run(
user_input = "Yes, go ahead",
deps = RunDeps(replyId = earlierRefId)
)

Error handling

A Run can complete successfully or fail. When awaiting run.done(), handle the two possible failure types separately, as each represents a different class of error.

try {
val answer = run.done()
println(answer.message)
} catch (e: AgentError) {
println("The run failed: [${e.code}] ${e.message}")
} catch (e: IllegalStateException) {
println("This run was replaced by a newer one.")
}
  • AgentError — Indicates that the Run failed. The ADK synthesizes two error codes:

    • HUMAN_INPUT_TIMEOUT — The user did not respond to a human_input_request before its timeout expired.
    • TRANSPORT_ERROR — The underlying WebSocket connection failed while the Run was in progress. The ADK converts this transport failure into an AgentError so that run.done() fails consistently with other runtime errors.

    All other error codes originate from the agent implementation. Treat agent-specific codes according to your application's requirements — for example, a "needs more information" error may indicate that additional user input is required, rather than a fatal failure.

  • StateError — Indicates that the Run was superseded by a newer Run on the same AgentThread. In most applications, this exception can be safely ignored.

Error codes

CodeMeaning
HUMAN_INPUT_TIMEOUTThe user did not respond to a human_input_request before its timeout expired
TRANSPORT_ERRORThe underlying WebSocket connection failed while the Run was in progress. The ADK converts this transport failure into an AgentError so that run.done() fails consistently with other runtime errors
RUN_REJECTEDThe run was rejected before producing a terminal event

Complete example

The following example starts a conversation, displays each event, answers a question automatically, and prints the result.

suspend fun talkToAgent(adk: Adk) {
// A handle to the agent, and a fresh conversation.
val agent = adk.agent("travel-planner")
val thread = agent.thread()

// Display each event as it arrives (registered before run()).
thread.listen { envelope: AgentEventEnvelope ->
println("event: ${envelope.event}")
}

// Respond to any question the agent asks. (Use real UI in your Application.)
thread.feedbackRequest { request: HumanInputRequest, run: Run ->
println("agent asks: ${request.prompt}")
if (!run.isClosed()) {
run.sendFeedback("Budget 50k, 20-25 December")
}
}

// Optional: progress telemetry.
thread.listenTrace { frame: Any? -> println("trace: $frame") }

// Send the prompt and wait for the final response.
val run = thread.run("Plan a 3-day trip to Goa")

try {
val answer = run.done()
println("Done: ${answer.message}")
} catch (e: AgentError) {
println("Failed: [${e.code}] ${e.message}")
} catch (e: IllegalStateException) {
println("Run superseded: ${e.message}")
}
}

API reference

Event types

All event bodies implement EnvelopeMeta (threadId, refId, agentId, replyTo).

ClassFields (beyond EnvelopeMeta)
AgentEventEnvelopeevent, content, isKnown
AgentOutputmessage, data?, metadata?
AgentErrorcode, message, details?
HumanInputRequestprompt, expectedResponseType, expectedResponseTypeRaw, context?, timeout?, schema?
AgentWaitwaitingForAgentId, invocationId?, reason?, timeout?, progress?
PlannerCorrectioncorrectionRequired, reason, newGoal?, suggestedAgents?
UnknownAgentEventevent, content (raw map)