Error handling
The crate-wide Error enum, its provider, tool, service, and schema sub-enums, and how failures surface in event streams.
Every public adk-rs API returns adk_rs::Result<T> — an alias for Result<T, adk_rs::Error>. The top-level Error enum wraps richer per-subsystem enums (provider, tool, service, schema) so call sites keep context while consumers match on a single type.
The Error enum
| Variant | Meaning |
|---|---|
Provider(ProviderError) | LLM provider client failure (transport, HTTP status, decoding, auth, rate limit, streaming). |
Tool(ToolError) | Tool invocation failure. |
Service(ServiceError) | Session / artifact / memory / credential backend failure. |
Schema(SchemaError) | Schema generation, sanitization, or validation failure. |
Config(String) | Invalid caller-supplied configuration — also what the security guards raise. |
NotFound(String) | The requested entity does not exist. |
AlreadyExists(String) | The entity already exists. |
InvalidInput(String) | Input validation failure (e.g. malformed args). |
Io(std::io::Error) | I/O failure (#[from]). |
Json(serde_json::Error) | JSON encode/decode failure (#[from]). |
Other(String) | Anything else, captured as a string. |
Shorthand constructors exist for the string variants: Error::config, Error::not_found, Error::already_exists, Error::invalid_input, and Error::other. A small Context trait adds anyhow-style context: result.context("loading eval set")? wraps any displayable error into Error::Other with the message prefixed.
Sub-enums
ProviderError
Transport(String)- HTTP transport failure — DNS, TLS, connection reset.
Http { status: u16, body: String }- Non-2xx response from the provider; the body is preserved (truncated when very large).
Decode(String)- 2xx response whose body could not be decoded.
Auth(String)- Missing API key or bad credentials.
RateLimit(String)- Provider rate limit exceeded — the canonical retry-with-backoff signal.
Stream(String)- Streaming (SSE) protocol violation.
Unsupported(&'static str)- The provider does not support a requested feature.
ToolError
InvalidArgs { tool: String, message: String }- Arguments did not match the tool schema.
Execution { tool: String, message: String }- The tool failed while running.
Aborted { tool: String, message: String }- The tool requested aborting the entire agent turn.
Unknown(String)- No tool of this name is registered with the agent.
ServiceError
SessionNotFound(String)- Session id unknown to the backend.
ArtifactNotFound(String)- Artifact key or version unknown.
StaleSession(String)- Optimistic-concurrency conflict: another writer updated the session.
Backend(String)- Database / storage failure (SQLite, Postgres, filesystem).
SchemaError
Sanitize(String)- The schema could not be sanitized for the target provider.
Invalid(String)- The schema itself is invalid.
How errors surface in event streams
A run produces a stream of Result<Event>, and failures travel down it on three distinct channels:
- Hard failures →
Erritems.LlmAgentruns insidetry_stream!, so a provider error fromgenerate_content(or an exceeded LLM-call budget) becomes anErr(Error)item that terminates the stream. The HTTP server maps these to a 500{"detail": ...}on/runor a finaldata: {"error": ...}SSE frame on/run_sse. - Soft model errors → error events. When the model responds but the response is itself a failure — a blocking
finish_reason, prompt feedback with ablock_reason, or an empty candidate — the resultingLlmResponsecarrieserror_code/error_messageinstead of content, and the event still flows asOk.LlmResponse::is_error()is the check; on the wire these serialize aserrorCode/errorMessage. The agent also synthesizes such events itself:MAX_ITERATIONSwhen the iteration budget is exhausted, andCANCELLEDwhen the cancellation token trips. - Tool failures → error payloads. A failed tool does not abort the turn: the agent converts the
Errinto a{"error": "<message>"}JSON function response and feeds it back to the model, which can apologise, retry, or pick another tool.
Matching on errors
use adk_rs::error::{Error, ProviderError};
use futures::StreamExt;
while let Some(item) = events.next().await {
match item {
Ok(event) => {
if event.response.is_error() {
eprintln!(
"model-level error: {:?} {:?}",
event.response.error_code, event.response.error_message
);
continue;
}
handle(event);
}
Err(Error::Provider(ProviderError::RateLimit(msg))) => {
// Back off and retry the turn.
eprintln!("rate limited: {msg}");
break;
}
Err(Error::Provider(ProviderError::Http { status, .. })) if status >= 500 => {
// Transient server error — also retryable.
break;
}
Err(e) => return Err(e),
}
}Note that cancellation never surfaces as an Err item: a cancelled run yields a final Ok event whose error_code is Some("CANCELLED"), caught by the is_error() branch above.
Error-recovery callbacks
adk_rs::core exports two error-hook callback type aliases: OnModelErrorCallback — Fn(&mut CallbackContext, &mut LlmRequest, &Error) -> Future<Result<Option<LlmResponse>>> — and OnToolErrorCallback — Fn(&mut ToolContext, &Arc<dyn DynTool>, &Value, &Error) -> Future<Result<Option<Value>>>. Per their contract, returning Ok(Some(...)) substitutes a recovery value for the failed call. Register them via LlmAgentBuilder::on_model_error_callback and ::on_tool_error_callback — a recovered model error never becomes an Err stream item; the substitute response continues the turn as if the call had succeeded. See Callbacks & plugins for the callback system and how hooks attach to the agent lifecycle; for run-level observation of failures, BasePlugin::after_run receives the run’s failure as Option<&Error>.
- Events — the
Event/LlmResponseshapes that carry soft errors. - Callbacks & plugins — lifecycle hooks, including the error callbacks.
- Providers — where
ProviderErrors originate.