Help us shape the future of Go! Take the 2026 Go Developer Survey

agenterr

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 2, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

README

agenterr

A tiny, dependency-free taxonomy for AI agent tool-call failures. When a tool call fails, agenterr tells your agent the one thing it needs to know: retry, re-prompt, or escalate.

Go Reference CI Go Report Card License

out := agenterr.Classify(resp.StatusCode, body, err)
switch {
case out.OK:                                  // 2xx — done
case out.Retryable:                            // 429/5xx/timeout — retry, don't touch the args
case out.Class == agenterr.ClassSelfCorrectable: // 400/404 — re-prompt with out.LLMHint
default:                                        // 401/403/unknown — escalate, don't rewrite the payload
}

The problem

Agents are great at making tool calls and terrible at handling the failures. The model gets back a raw 403, a swallowed exception, or a 2-page HTML error, and has no principled way to react. So it does the wrong thing:

  • Re-prompts on a 503 — burns tokens and often corrupts a payload that was fine.
  • Retries a 400 forever — the arguments are wrong; looping won't fix them.
  • Rewrites the payload on a 401 — no edit to the body will ever fix an expired token.

The useful question was never "what's the HTTP status." It's "how should the agent respond." agenterr answers exactly that and stops there.

The taxonomy

Every failure maps to one code, and every code to one class — three mutually exclusive reactions, plus an explicit unknown:

Class Codes Agent reaction
transient RATE_LIMITED · TIMEOUT · UNAVAILABLE Retry with backoff. Don't change the request.
self_correctable SCHEMA_INVALID · INVALID_REQUEST · NOT_FOUND Feed LLMHint back to the model, fix the args, resend.
auth UNAUTHORIZED Fix credentials/scope out of band. Don't let the model rewrite the payload.
indeterminate UNKNOWN Escalate to a human.

The full contract — status→code rules, the JSON shape, Retry-After handling — is in SPEC.md, written to be portable across languages.

The hint is the point

Each failure carries an LLMHint written to be pasted verbatim into the model's context. The trick isn't just telling the model how to fix a bad payload — it's telling it, on a transient or auth failure, to leave a good one alone:

FromStatus(401, `{"error":"token expired"}`).LLMHint
→ "The API rejected this call as unauthorized (HTTP 401). This is a credentials
   or permissions problem, NOT a problem with your arguments — do not modify
   them. A human or the orchestrator must fix authentication (an invalid or
   expired token, or a missing scope) before retrying. The API said: {"error":
   "token expired"}"

Install

go get github.com/chrassy/agenterr

Go 1.21+. No dependencies.

Use it

Classify an HTTP result (status + body + transport error, any of which may be zero):

out := agenterr.Classify(resp.StatusCode, body, err)

Or hand it the response directly — this also reads Retry-After:

out := agenterr.FromResponse(resp) // out.RetryAfter is set for 429/503 w/ the header

Catch a bad payload before you even make the call — same taxonomy, no round trip. Bring your own validator; agenterr just standardizes the code and hint:

if problems := myschema.Validate(args); len(problems) > 0 {
    out := agenterr.SchemaInvalid(problems...) // → SCHEMA_INVALID + a ready-to-paste hint
}

Every Outcome also serializes to a stable JSON shape (see the spec) so polyglot agent stacks agree on the wire.

Why not just…

  • …retry everything? You'll loop on 400/404/401 — failures where the request, not the network, is wrong.
  • …re-prompt the model on every error? You'll waste tokens on transient blips and corrupt good arguments. Only self_correctable failures should reach the model.
  • …map status codes inline? Everyone does, subtly differently, in every tool. agenterr makes it one shared, tested vocabulary — and it handles the parts people skip: transport-error timeouts, Retry-After, bounded body snippets, and the "don't touch the payload" guidance that actually prevents regressions.

Scope (on purpose)

agenterr classifies and advises. It never executes, retries, or persists. There is no queue, no backoff loop, no circuit breaker, no webhook in here — and there won't be. It's a decision function you drop into whatever runtime you already have.

The moment you act on Retryable: true in a way that has to survive a process restart, not block your agent for 60 seconds, and leave an audit trail, you're building durable execution. You can build that yourself on top of this — or use a managed one:

klanex runs this taxonomy as a service: submit a tool-use intent and it does the durable retries, per-host circuit breaking, credential sealing, and signed webhooks for you — returning these exact error codes and hints. agenterr is the open, standalone taxonomy; klanex is the managed engine that acts on it. Use the taxonomy with or without it.

Ports

The Go package is the reference implementation. Ports that satisfy SPEC.md are very welcome — Python and TypeScript are the priorities; open an issue if you're starting one.

License

Apache 2.0.

Documentation

Overview

Package agenterr classifies the failures of an AI agent's tool calls into a small, stable taxonomy — so an agent (or the orchestration around it) can tell at a glance whether to retry, re-prompt the model to fix its arguments, or stop and escalate. It has one job and no dependencies.

The problem

When an agent's tool call fails, the model gets a stack trace, a raw 4xx, or a swallowed exception — and has no principled way to decide what to do next. Re-prompting on a 503 wastes tokens and corrupts good arguments; blindly retrying a 400 loops forever; retrying a 401 will never succeed no matter how the payload is rewritten. The useful question is not "what is the HTTP status" but "how should the agent respond".

The taxonomy

Every failure maps to one Code, and every Code to one Class — three mutually exclusive reactions plus an explicit unknown:

  • ClassTransient (RATE_LIMITED, TIMEOUT, UNAVAILABLE): not the request's fault. Retry with backoff; the model changes nothing.
  • ClassSelfCorrectable (SCHEMA_INVALID, INVALID_REQUEST, NOT_FOUND): the arguments are fixable. Feed Outcome.LLMHint back to the model and resend.
  • ClassAuth (UNAUTHORIZED): credentials or scope, not the payload. Do not let the model rewrite the arguments; fix auth out of band.
  • ClassIndeterminate (UNKNOWN): could not be classified. Escalate.

Usage

Classify a tool call's result and branch on the response class:

out := agenterr.Classify(resp.StatusCode, body, err)
switch {
case out.OK:
	// success — nothing to do
case out.Retryable:
	// transient — retry with backoff; leave the request untouched
case out.Class == agenterr.ClassSelfCorrectable:
	// feed out.LLMHint back into the model's context, then resend
default:
	// auth or unknown — escalate; do not let the model rewrite the payload
}

The library only decides and advises. It never performs the call, retries, or persists anything: that durable-execution machinery is deliberately out of scope (see the README). SchemaInvalid lets you emit the same taxonomy for a pre-flight validation failure without making a call at all.

See SPEC.md for the language-agnostic contract that ports must satisfy.

Example

The canonical agent loop: classify a tool call's result, then react by class — retry, re-prompt, or escalate — without re-inventing that logic per tool.

package main

import (
	"fmt"

	"github.com/chrassy/agenterr"
)

func main() {
	// Pretend a tool call to some API came back 422 with a validation message.
	status, body, err := 422, `{"error":"amount must be a positive integer"}`, error(nil)

	out := agenterr.Classify(status, body, err)
	switch {
	case out.OK:
		fmt.Println("done")
	case out.Retryable:
		fmt.Println("retry with backoff, don't touch the arguments")
	case out.Class == agenterr.ClassSelfCorrectable:
		fmt.Println("re-prompt the model with this hint, then resend:")
		fmt.Println(out.LLMHint)
	default:
		fmt.Println("escalate:", out.Code)
	}
}
Output:
re-prompt the model with this hint, then resend:
The API rejected this call as invalid (HTTP 422). Your arguments are wrong in a way you can fix: correct them to satisfy the API's requirements and resend. The API said: {"error":"amount must be a positive integer"}

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Succeeded

func Succeeded(status int) bool

Succeeded reports whether an HTTP status is a success (2xx), so callers can gate classification without duplicating the check.

Types

type Class

type Class string

Class groups codes by how an agent should respond to them. It is the heart of the taxonomy: the four classes are three mutually exclusive reactions (retry, re-prompt, escalate) plus an explicit "don't know".

const (
	// ClassTransient: the failure is temporary and not caused by the request.
	// The caller should retry with backoff; the model must not change anything.
	ClassTransient Class = "transient"
	// ClassSelfCorrectable: the request was wrong in a way the model can fix.
	// Feed Outcome.LLMHint back into the model's context and resend.
	ClassSelfCorrectable Class = "self_correctable"
	// ClassAuth: an authentication or permission problem. The payload is not at
	// fault; a human or the orchestrator must fix credentials or scope. Do not
	// let the model rewrite the arguments.
	ClassAuth Class = "auth"
	// ClassIndeterminate: the failure could not be classified. Surface it to a
	// human rather than blindly retrying.
	ClassIndeterminate Class = "indeterminate"
)

type Code

type Code string

Code is a stable, machine-readable classification of a tool-call failure. Every Code belongs to exactly one Class, which is what tells an agent how to respond. Codes and their metadata are the public contract of this package; see SPEC.md for the language-agnostic definition.

const (
	// CodeSchemaInvalid: the arguments the model produced failed a pre-flight
	// schema or shape check, before any call was made. Self-correctable.
	CodeSchemaInvalid Code = "SCHEMA_INVALID"
	// CodeInvalidRequest: the API rejected the request as malformed or
	// semantically invalid (typically 400 or 422). Self-correctable.
	CodeInvalidRequest Code = "INVALID_REQUEST"
	// CodeNotFound: the API returned 404 — a referenced resource does not
	// exist (often a hallucinated identifier). Self-correctable.
	CodeNotFound Code = "NOT_FOUND"
	// CodeUnauthorized: the API rejected the request for authentication or
	// permission reasons (401 or 403). Not a payload problem.
	CodeUnauthorized Code = "UNAUTHORIZED"
	// CodeRateLimited: the API is rate-limiting the caller (429). Transient.
	CodeRateLimited Code = "RATE_LIMITED"
	// CodeTimeout: the API did not respond in time — a 408 or a client-side
	// timeout with no response. Transient.
	CodeTimeout Code = "TIMEOUT"
	// CodeUnavailable: the API is temporarily unavailable — a 5xx response or
	// a transport/network failure. Transient.
	CodeUnavailable Code = "UNAVAILABLE"
	// CodeUnknown: the result could not be classified. Treat conservatively.
	CodeUnknown Code = "UNKNOWN"
)

func Codes

func Codes() []Code

Codes returns every defined code in a stable order (severity-agnostic; grouped self-correctable, auth, transient, indeterminate). Useful for docs, tests, and building UIs.

func (Code) Class

func (c Code) Class() Class

Class returns the response class for c.

func (Code) Meta

func (c Code) Meta() Meta

Meta returns the fixed metadata for c. An unrecognized code returns the metadata for CodeUnknown, so callers never get a zero Meta.

func (Code) Retryable

func (c Code) Retryable() bool

Retryable reports whether c is a transient failure the caller should retry.

func (Code) String

func (c Code) String() string

String implements fmt.Stringer.

type Meta

type Meta struct {
	Code        Code
	Class       Class
	Retryable   bool
	Summary     string // one-line description of the failure
	AgentAction string // what the agent or orchestrator should do about it
}

Meta is the fixed, human-readable metadata for a Code.

type Outcome

type Outcome struct {
	// OK is true when there was nothing to classify: a 2xx result, or a nil
	// error passed to FromError/Classify. The other fields are then zero.
	OK bool `json:"ok"`
	// Code is the classification. Empty only when OK is true.
	Code Code `json:"code,omitempty"`
	// Class mirrors Code.Class(); included so logs and cross-language consumers
	// don't need the registry to interpret the outcome.
	Class Class `json:"class,omitempty"`
	// Retryable mirrors Code.Retryable(): true for the transient class only.
	Retryable bool `json:"retryable"`
	// LLMHint is phrased to be injected verbatim into the model's context. It
	// is present for every failure — for transient and auth codes it tells the
	// model NOT to change the request, which is just as important as telling it
	// how to fix one.
	LLMHint string `json:"llm_hint,omitempty"`
	// RetryAfter is the server-advised minimum delay before retrying, parsed
	// from a Retry-After header by FromResponse (0 when absent or not parsed).
	RetryAfter time.Duration `json:"-"`
	// Status is the HTTP status that produced this outcome, or 0 for a
	// transport-level failure or a pre-flight (SchemaInvalid) outcome.
	Status int `json:"status,omitempty"`
}

Outcome is the classification of a single tool-call attempt. It is the value you branch on: OK short-circuits success, Retryable covers the transient class, and Class distinguishes self-correctable from auth/unknown failures.

func Classify

func Classify(status int, body string, err error) Outcome

Classify is the combined entry point. A non-nil err is treated as a transport-level failure (no response) and takes precedence over status/body; otherwise the call is classified by status and body.

func FromError

func FromError(err error) Outcome

FromError classifies a transport-level failure that produced no HTTP response — a client timeout, connection refused, DNS failure, etc. A context deadline or any net.Error reporting Timeout() maps to TIMEOUT; everything else maps to UNAVAILABLE. A nil error returns Outcome{OK: true}.

Example

A transport failure with no HTTP response classifies from the error alone.

package main

import (
	"errors"
	"fmt"

	"github.com/chrassy/agenterr"
)

func main() {
	out := agenterr.FromError(errors.New("dial tcp: connection refused"))
	fmt.Println(out.Code, "status:", out.Status)
}
Output:
UNAVAILABLE status: 0

func FromResponse

func FromResponse(resp *http.Response) Outcome

FromResponse classifies an *http.Response. Beyond FromStatus it reads the Retry-After header (seconds or HTTP-date) into Outcome.RetryAfter and quotes a bounded snippet of the body into the hint. It reads up to a small cap from resp.Body and does NOT close it — the caller still owns the body.

func FromStatus

func FromStatus(status int, body string) Outcome

FromStatus classifies a tool call by its HTTP status code. body (which may be empty) enriches the hint for a self-correctable failure; it is trimmed, single-lined, and length-capped before being quoted. A 2xx status returns Outcome{OK: true}.

Example (Auth)

An auth failure is NOT self-correctable: rewriting the payload can't fix it.

package main

import (
	"fmt"

	"github.com/chrassy/agenterr"
)

func main() {
	out := agenterr.FromStatus(403, "")
	fmt.Println(out.Code, out.Class, "retryable:", out.Retryable)
}
Output:
UNAUTHORIZED auth retryable: false
Example (Transient)

A transient failure: the hint tells the model to leave the request alone.

package main

import (
	"fmt"

	"github.com/chrassy/agenterr"
)

func main() {
	out := agenterr.FromStatus(503, "upstream connect error")
	fmt.Println(out.Code, "retryable:", out.Retryable)
}
Output:
UNAVAILABLE retryable: true

func SchemaInvalid

func SchemaInvalid(problems ...string) Outcome

SchemaInvalid builds a SCHEMA_INVALID outcome from a list of human-readable validation problems and formats them into an LLMHint the model can act on. Use it when arguments fail your own pre-flight schema check, before any call is made — catching the mistake here saves a network round trip. The library performs no JSON Schema validation itself; bring your own validator and pass its messages.

Example

Catch a bad payload before spending a network round trip on it.

package main

import (
	"fmt"

	"github.com/chrassy/agenterr"
)

func main() {
	out := agenterr.SchemaInvalid(`at "/": missing properties: 'charge_id'`)
	fmt.Println(out.Code)
	fmt.Println(out.LLMHint)
}
Output:
SCHEMA_INVALID
The arguments you generated do not match the required schema. Fix the following and resend:
- at "/": missing properties: 'charge_id'

func (Outcome) Failed

func (o Outcome) Failed() bool

Failed reports whether the outcome represents a failure (the inverse of OK).

func (Outcome) MarshalJSON

func (o Outcome) MarshalJSON() ([]byte, error)

MarshalJSON renders RetryAfter as whole seconds under "retry_after_seconds", so the serialized shape matches SPEC.md across language ports.

func (Outcome) String

func (o Outcome) String() string

String renders a compact form for logs.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL