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.
  • An agent has been created and deployed in the Agent Builder, and you have its agent id.

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.
let agent: Agent = adk.agent("your-agent-id")

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

// 3. Send a prompt and wait for the final response.
let run: Run = try await thread.run("Plan a 3-day trip to Goa")
let result: AgentOutput = try await run.done()
print(result.message);

// 4.Listen for agent events
await thread.listen { envelope in
switch envelope.payload
{
...
}
}

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:

let 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:

let thread: AgentThread = agent.thread()

print("Conversation started: \(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:

do {
let run: Run = try await thread.run("Plan a 3-day trip to Goa")

let output: AgentOutput = try await run.done()

print(output.message)

} catch let error as AgentError {
print("[\(error.code)] \(error.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:

Task {
await thread.listen { envelope in
switch envelope.payload {

case .output(let output):
print("Agent: \(output.message)")

case .error(let error):
print("Error [\(error.code)]: \(error.message)")

case .humanInput(let request):
print("Agent asks: \(request.prompt)")

case .wait(let wait):
print("Waiting for agent: \(wait.waitingForAgentId)")

case .plannerCorrection(let correction):
print("Planner correction: \(correction.reason)")

case .unknown(let event):
print("Unknown event \(event.event): \(event.content)")
}
}
}

Event catalog

MemberTypeDescription
.outputAgentOutputTerminal response from the agent
.errorAgentErrorError returned by the agent
.humanInputHumanInputRequestAgent requires user input
.waitAgentWaitAgent is waiting for another agent
.plannerCorrectionPlannerCorrectionPlanner requested a correction
.unknownUnknownAgentEventRaw event not modelled by the SDK

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.

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 { frame in
print(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:

do {
let answer: AgentOutput = try await run.done()
print(answer.message)
} catch let error as AgentError {
print("The run failed: [\(error.code)] \(error.message)")
} catch is StateError {
print("This run was replaced by a newer one.")
} catch {
print("Unexpected error: \(error.localizedDescription)")
}

Error codes

CodeMeaning
HUMAN_INPUT_TIMEOUTThe user did not respond to a human_input_request before its timeout expired
TRANSPORT_ERRORThe underlying WebSocket errored mid-run
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:

// Display each event as it arrives (registered before run()).
func talkToAgent(adk: Adk) async {
// A handle to the agent, and a fresh conversation.
let agent: Agent = adk.agent("your-agent-id")
let thread: AgentThread = agent.thread()

// Display each event as it arrives (registered before run()).
await thread.listen { (envelope: AgentEventEnvelope) in
print("event: \(envelope.event)")
}

// Respond to any question the agent asks. (Use real UI in your application.)
thread.feedbackRequest { (request: HumanInputRequest, run: Run) async in
print("agent asks: \(request.prompt)")

if !run.isClosed() {
await run.sendFeedback("Budget 50k, 20-25 December")
}
}

// Optional: progress telemetry.
await thread.listenTrace { frame in
print("trace: \(frame)")
}

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

do {
let answer: AgentOutput = try await run.done()
print("Done: \(answer.message)")
} catch let error as AgentError {
print("Failed: [\(error.code)] \(error.message)")
} catch is StateError {
print("Run superseded.")
} catch {
print("Unexpected error: \(error.localizedDescription)")
}
}