Agents
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.
| Class | Description |
|---|---|
Agent | Represents a single AI agent identified by its agentId. Reuse the same Agent instance for all conversations with that agent. |
AgentThread | Represents 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. |
Run | Represents 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.
Agent agent = adk.Agent('your-agent-id');
AgentThread thread = agent.Thread();;
// 2. Send a prompt and wait for the final response.
Run run = await thread.Run('Plan a 3-day trip to Goa');
AgentOutput output = await run.Done();
Log(output.message);
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.
Step 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:
// Opens the agent_com_<agentId> subscription
Agent agent = adk.Agent("your-agent-id").Connect();
Step 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();
print(thread.threadId);
Step 3 — Send 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.
try
{
// Start the run
Run run = await thread.Run("Plan a 3-day trip to Goa");
// Wait until the final agent_output terminal response is received
AgentOutput output = await run.Done();
Log(output.Message);
}
catch (AgentRunException e)
{
Debug.LogError($"Agent run failed: {e.Message} (Error Code: {e.Error.Code})");
}
The following sequence diagram shows the complete lifecycle of a run when the agent does not request human input.
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.
Receiving 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().
// Register the listener (awaits the setup of the connection listener internally)
await thread.Listen((AgentEventEnvelope envelope) =>
{
switch (envelope.Event)
{
// Terminal success: yields the AgentOutput payload
case "agent_output":
var output = envelope.Content as AgentOutput;
Log($"Answer: {output.Message}");
break;
// Mid-run prompt for the user (Human-in-the-loop request)
case "human_input_request":
var hitl = envelope.Content as HumanInputRequest;
Log($"Agent needs input: {hitl.Prompt}");
break;
// Terminal failure: yields the AgentError payload
case "agent_error":
var error = envelope.Content as AgentError;
Debug.LogError($"Error [{error.Code}]: {error.Message}");
break;
// Waiting on another agent
case "agent_wait":
var wait = envelope.Content as AgentWait;
Log($"Waiting for agent: '{wait.WaitingForAgentId}'. Reason: {wait.Reason}");
break;
// Planner re-plan
case "planner_correction":
var correction = envelope.Content as PlannerCorrection;
Log($"Revising plan: {correction.Reason}");
break;
// Any custom or untyped events arrive as UnknownAgentEvent
default:
var unknown = envelope.Content as UnknownAgentEvent;
Log('Unknown event ${unknown.event}: ${unknown.content}');
break;
}
});
The listener callback receives an AgentEventEnvelope for every inbound event.
| Member | Type | Description |
|---|---|---|
event | String | The event name. Use this value to determine the event type and dispatch the corresponding handler. |
content | Object | The typed event payload. Cast it to the appropriate class based on the value of event. |
isKnown | bool | Indicates whether the event is one of the event types recognized by the ADK. |
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— TherefIdof the event to which this event is responding.
These fields may contain empty strings if the server does not populate them.
| Event | Body | Ends the run? | Key fields |
|---|---|---|---|
agent_general_response | AgentOutput | Yes — run.done() completes successfully. | message, data, metadata |
agent_error_response | AgentError | Yes — run.done() throws an AgentError. | code, message, details |
human_input_request | HumanInputRequest | No — the run waits for user input. | prompt, expectedResponseType, timeout, context, schema |
agent_wait_response | AgentWait | No — progress notification only. | waitingForAgentId, reason, progress, timeout |
planner_correction_request | PlannerCorrection | No — progress notification only. | correctionRequired, reason, newGoal, suggestedAgents |
Diagnostics with trace
While a Run is in progress, the agent may emit trace frames containing diagnostic and telemetry information, such as heartbeats, checkpoints, and deadlock-detection signals. Trace frames are delivered separately from the normal event stream and are intended for observability, progress reporting, and debugging.
await thread.listenTrace((dynamic frame) {
Log('trace: $frame');
});
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
{
// Start the run
Run run = await thread.Run("Plan a 3-day trip to Goa");
// Wait until the final agent_output terminal response is received
AgentOutput output = await run.Done();
Log(output.Message);
}
catch (AgentRunException e)
{
Debug.LogError($"Agent run failed: {e.Message} (Error Code: {e.Error.Code})");
}
-
AgentError— Indicates that theRunfailed. The ADK synthesizes two error codes:HUMAN_INPUT_TIMEOUT— The user did not respond to ahuman_input_requestbefore its timeout expired.TRANSPORT_ERROR— The underlying WebSocket connection failed while theRunwas in progress. The ADK converts this transport failure into anAgentErrorso thatrun.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 theRunwas superseded by a newerRunon the sameAgentThread. In most applications, this exception can be safely ignored.
Complete example
The following example starts a conversation, displays each event, answers a question automatically, and prints the result.
Future<void> talkToAgent(Adk adk) async {
// A handle to the agent, and a fresh conversation.
Agent agent = adk.Agent("travel-planner").Connect();
AgentThread thread = agent.Thread();
// Display each event as it arrives (registered before run()).
await thread.listen((AgentEventEnvelope envelope) {
Log('event: ${envelope.event}');
});
// Respond to any question the agent asks. (Use real UI in your Application.)
thread.OnHumanInput(async (HumanInputRequest request, Run activeRun) =>
{
Log($"HITL Prompt: {request.Prompt}");
// Collect response from user input fields or options
string userReply = "Approve invoice #1002";
// Resume agent loop with the answer payload
await activeRun.SendFeedback(userReply);
});
// Optional: progress telemetry.
await thread.listenTrace((dynamic frame) => print('trace: $frame'));
// Send the prompt and wait for the final response.
try
{
// Start the run
Run run = await thread.Run("Plan a 3-day trip to Goa");
// Wait until the final agent_output terminal response is received
AgentOutput output = await run.Done();
Log(output.Message);
}
catch (AgentRunException e)
{
Debug.LogError($"Agent run failed: {e.Message} (Error Code: {e.Error.Code})");
}
API reference
Event types
All event bodies implement EnvelopeMeta (threadId, refId, agentId, replyTo).
| Class | Fields (beyond EnvelopeMeta) |
|---|---|
AgentEventEnvelope | event, content, isKnown |
AgentOutput | message, data?, metadata? |
AgentError | code, message, details? |
HumanInputRequest | prompt, expectedResponseType, expectedResponseTypeRaw, context?, timeout?, schema? |
AgentWait | waitingForAgentId, invocationId?, reason?, timeout?, progress? |
PlannerCorrection | correctionRequired, reason, newGoal?, suggestedAgents? |
UnknownAgentEvent | event, content (raw map) |
