# ADR-0032: A2A Error Handling and JSON-RPC 2.0 Compliance

**Status**: Implemented
**Date**: 2026-02-07
**Deciders**: VAPORA Team
**Technical Story**: Consistent, specification-compliant error representation across the A2A client/server boundary

---

## Decision

Two-layer error handling strategy for the A2A subsystem:

**Layer 1 — Domain errors (Rust `thiserror`):**

```rust
// vapora-a2a
pub enum A2aError {
    TaskNotFound(String),
    InvalidStateTransition { current: String, target: String },
    CoordinatorError(String),
    UnknownSkill(String),
    SerdeError,
    IoError,
    InternalError(String),
}

// vapora-a2a-client
pub enum A2aClientError {
    HttpError,
    TaskNotFound(String),
    ServerError { code: i32, message: String },
    ConnectionRefused(String),
    Timeout(String),
    InvalidResponse,
    InternalError(String),
}
```

**Layer 2 — Protocol serialization (JSON-RPC 2.0):**

```rust
impl A2aError {
    pub fn to_json_rpc_error(&self) -> serde_json::Value {
        json!({
            "jsonrpc": "2.0",
            "error": { "code": <domain-code>, "message": <message> }
        })
    }
}
```

**Error code mapping:**

| Category | JSON-RPC Code | A2aError variants |
|---|---|---|
| Domain / server errors | -32000 | `TaskNotFound`, `UnknownSkill`, `InvalidStateTransition` |
| Internal errors | -32603 | `SerdeError`, `IoError`, `InternalError` |
| Parse errors | -32700 | Handled by JSON parser |
| Invalid request | -32600 | Handled by Axum |

---

## Rationale

**Why two layers?** Domain layer gives type-safe `Result<T, A2aError>` propagation throughout the crate. Protocol layer isolates JSON-RPC specifics to conversion methods — domain code has no protocol awareness.

**Why JSON-RPC 2.0 standard codes?** Code ranges are defined by the specification and understood by compliant clients without custom documentation. Enables generic error handling on the client side.

**Why `thiserror`?** Minimal boilerplate. Automatic `Display` derives. Composes cleanly with `?`. Validated pattern throughout the VAPORA codebase (ADR-0022).

**Why one-way conversion (domain → protocol)?** Protocol details cannot bleed into domain code. Future protocol changes are contained to conversion methods. Each layer is independently testable.

---

## Alternatives Considered

**Custom error codes** — rejected: non-standard, client libraries can't handle them generically, harder to debug.

**Single error type** — rejected: collapses domain semantics into protocol representation, loses type safety, makes specific error handling impossible.

**No protocol conversion (raw Rust errors as HTTP 500)** — rejected: violates JSON-RPC 2.0 compliance, breaks A2A client expectations, prevents interoperability.

---

## Trade-offs

**Pros:**

- Compile-time exhaustive error handling via `match`
- Protocol compliance verified: clients receive spec-compliant `{"jsonrpc":"2.0","error":{...}}`
- Error flow is auditable — each variant maps to exactly one JSON-RPC code
- Contextual tracing: all errors logged with `task_id`, `operation`, error message
- Client retry logic (`RetryPolicy`) classifies errors from JSON-RPC codes: 5xx retried, 4xx not retried

**Cons:**

- Some error context is intentionally lost in translation (internal detail not exposed to clients)
- JSON-RPC code documentation must be kept in sync with new variants
- Boundary conversions require explicit calls at each Axum handler

---

## Implementation

**Key files:**

- `crates/vapora-a2a/src/error.rs` — `A2aError` + `to_json_rpc_error()`
- `crates/vapora-a2a-client/src/error.rs` — `A2aClientError`
- `crates/vapora-a2a-client/src/retry.rs` — Error classification for retry policy

**Error flow:**

```text
HTTP request
    → Axum handler
    → TaskManager::get(id) → Err(A2aError::TaskNotFound)
    → to_json_rpc_error() → {"jsonrpc":"2.0","error":{"code":-32000,...}}
    → (StatusCode::NOT_FOUND, Json(error_body))
    ← vapora-a2a-client parses → A2aClientError::TaskNotFound
    ← caller matches variant
```

---

## Verification

```bash
cargo test -p vapora-a2a                  # error conversion tests
cargo test -p vapora-a2a-client           # 5/5 pass (includes retry classification)
cargo clippy -p vapora-a2a -- -D warnings
cargo clippy -p vapora-a2a-client -- -D warnings
```

---

## Consequences

- All new A2A error variants must be added to both `A2aError` and the JSON-RPC code mapping table
- `A2aClientError` must mirror any new server-side variants that clients need to handle specifically
- Pattern is scoped to the A2A subsystem; general VAPORA error handling follows ADR-0022

---

## References

- `crates/vapora-a2a/src/error.rs`
- `crates/vapora-a2a-client/src/error.rs`
- [thiserror](https://docs.rs/thiserror/)
- [JSON-RPC 2.0 Specification](https://www.jsonrpc.org/specification)
- [Axum error responses](https://docs.rs/axum/latest/axum/response/index.html)

**Related ADRs:**

- [ADR-0030](./0030-a2a-protocol-implementation.md) — A2A protocol (server that produces these errors)
- [ADR-0022](./0022-error-handling.md) — General two-tier error handling pattern (this ADR specializes it for A2A/JSON-RPC)
