status

package
v1.12.0 Latest Latest
Warning

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

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

Documentation

Overview

Package status defines Genkit's canonical status codes and the error type that carries them.

Classifying an error

Error is the only error type Genkit defines. It pairs a message with a canonical status Name and the Sentinel that classified it. Build one with Errorf, whose first argument is the sentinel:

return status.Errorf(status.ErrNotFound, "model %q not found", name)

Callers branch with errors.Is rather than by matching message text:

if errors.Is(err, ai.ErrMaxTurnsExceeded) { ... } // specific
if errors.Is(err, status.ErrAborted) { ... }      // broad

A base sentinel exists for every status (ErrInvalidArgument, ErrNotFound, ErrAborted, ...). Packages declare domain sentinels from them with Sentinel.Subtype, which inherits the status and still matches the parent:

var ErrMaxTurnsExceeded = status.ErrAborted.Subtype("max turns exceeded")

Adding context versus reclassifying

Classify at the point where the failure mode is actually known, which is usually deep in the call stack: the code that looked up the model is the only code that knows a missing model is NotFound, not the HTTP handler ten frames up. Everything above it should add context without touching the classification:

return fmt.Errorf("agent %q: %w", name, err) // status and sentinel survive

Reclassify only at a boundary where the meaning genuinely changes, and do it deliberately with Errorf. A tool's own NotFound, for instance, is not a NotFound for the request that invoked the tool; it is an Internal failure of that tool:

return status.Errorf(status.ErrInternal, "tool %q failed: %w", name, err)

When several Error values are in one chain, errors.As finds the outermost, so the last deliberate reclassification is the one transports report. Of follows the same rule.

Of answers Internal for an unclassified error, which is right for a transport but not for code deciding what to do about the failure. Middleware branching on a status wants Classified, which reports whether anything in the chain actually carried one.

The pattern to avoid is restating the status on every frame as an error bubbles up. Wrapping with %v is the usual culprit: it flattens the cause into a string, so the sentinel, the status, and everything else in the chain are lost.

Messages

Keep messages short and specific, and name the thing that failed: the action key, model name, tool name, session or snapshot ID. Prefer

status.Errorf(status.ErrNotFound, "tool %q not found", name)

over a generic "tool not found", and do not prefix messages with the name of the unexported function that produced them. Genkit composes the surrounding context by wrapping, so a message only needs to describe its own layer.

Reaching clients

Errorf produces an error whose message stays server-side. PublicErrorf marks a message as safe to return over the wire:

return status.PublicErrorf(status.ErrInvalidArgument, "invalid %q parameter", param)

Transports call PublicMessage, which returns the message only when the outermost Error is public and a generic string derived from the status otherwise. The status code itself is always reported.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrCancelled          = NewSentinel(Cancelled, "cancelled")
	ErrUnknown            = NewSentinel(Unknown, "unknown")
	ErrInvalidArgument    = NewSentinel(InvalidArgument, "invalid argument")
	ErrDeadlineExceeded   = NewSentinel(DeadlineExceeded, "deadline exceeded")
	ErrNotFound           = NewSentinel(NotFound, "not found")
	ErrAlreadyExists      = NewSentinel(AlreadyExists, "already exists")
	ErrPermissionDenied   = NewSentinel(PermissionDenied, "permission denied")
	ErrUnauthenticated    = NewSentinel(Unauthenticated, "unauthenticated")
	ErrResourceExhausted  = NewSentinel(ResourceExhausted, "resource exhausted")
	ErrFailedPrecondition = NewSentinel(FailedPrecondition, "failed precondition")
	ErrAborted            = NewSentinel(Aborted, "aborted")
	ErrOutOfRange         = NewSentinel(OutOfRange, "out of range")
	ErrUnimplemented      = NewSentinel(Unimplemented, "unimplemented")
	ErrInternal           = NewSentinel(Internal, "internal")
	ErrUnavailable        = NewSentinel(Unavailable, "unavailable")
	ErrDataLoss           = NewSentinel(DataLoss, "data loss")
)

Base sentinels, one per status name. Reach for these when no more specific sentinel fits; otherwise prefer (or declare) a domain sentinel via Sentinel.Subtype so callers can branch on the actual failure mode.

View Source
var (
	// ErrInvalidSchema means an action's declared input or output schema could
	// not be resolved or compiled. The schema itself is wrong, not the value.
	ErrInvalidSchema = ErrInvalidArgument.Subtype("invalid schema")

	// ErrInvalidInput means a value failed validation against an action's input
	// schema, e.g. a model produced malformed tool arguments.
	ErrInvalidInput = ErrInvalidArgument.Subtype("invalid input")

	// ErrInvalidOutput means an action or model produced a value that does not
	// match the declared output schema. The fault is on the producing side,
	// not the caller's request, hence Internal.
	ErrInvalidOutput = ErrInternal.Subtype("invalid output")

	// ErrActionNotFound means no action is registered under the requested key.
	ErrActionNotFound = ErrNotFound.Subtype("action not found")

	// ErrPanic means a user-supplied function panicked and the framework
	// recovered at an action boundary.
	ErrPanic = ErrInternal.Subtype("panic")
)

Framework-level sentinels for failures the action machinery raises. Domain sentinels live with the package that raises them (see ai.ErrModelNotFound, streaming.ErrStreamNotFound, and friends).

Functions

func PublicMessage

func PublicMessage(err error) (msg string, public bool)

PublicMessage returns a message for err that is safe to show a client, and whether it came from the error itself. When the outermost Error is public its Message is returned verbatim; otherwise the result is a generic string derived from the status, so internal details never reach the client.

Transports should use this instead of err.Error(). Note that the fallback is deliberately uninformative: log err separately for diagnosis.

Example

Transports report the message only when it was marked safe to return.

package main

import (
	"fmt"

	"github.com/firebase/genkit/go/core/status"
)

func main() {
	internal := status.Errorf(status.ErrPermissionDenied, "user alice lacks role admin")
	public := status.PublicErrorf(status.ErrInvalidArgument, "invalid %q parameter", "stream")

	for _, err := range []error{internal, public} {
		msg, ok := status.PublicMessage(err)
		fmt.Printf("%d %q (public=%v)\n", status.Of(err).HTTPCode(), msg, ok)
	}
}
Output:
403 "permission denied" (public=false)
400 "invalid \"stream\" parameter" (public=true)

Types

type Error

type Error struct {
	// Status is the canonical status name for this failure. Wire field "status".
	Status Name
	// Message describes the failure. Wire field "message".
	Message string
	// Public reports whether Message is safe to return to a client. Transports
	// replace the message of a non-public error with a generic one so internal
	// details do not leak. Not serialized.
	Public bool
	// Details is optional structured information about the failure.
	// Wire field "details" (omitted when empty).
	Details map[string]any

	// HTTPCode is the HTTP status for Status, recorded at construction.
	//
	// Deprecated: use Status.HTTPCode(), which is correct for every Error
	// including ones built as a struct literal. This field exists so
	// core.GenkitError can alias Error, and will be removed with it.
	HTTPCode int

	// Source names the component that raised the error.
	//
	// Deprecated: never populated. It exists so core.GenkitError can alias
	// Error, and will be removed with it.
	Source *string
	// contains filtered or unexported fields
}

Error is Genkit's error type. It carries a canonical Name status, the Sentinel that classified it, and any wrapped cause.

On the wire an Error marshals to the canonical Genkit error shape ({status, message, details}), which mirrors the RuntimeError definition in the shared JSON schema. Fields that exist only in-process (Public, the sentinel, the cause, the stack) are not serialized.

Construct one with Errorf or PublicErrorf. To add context to an existing error without reclassifying it, use fmt.Errorf with %w instead.

Nil receivers

Error's methods, and the package functions that inspect an error, tolerate a nil *Error. This matters because Genkit hands out *Error in places that are nil in the ordinary case: Convert returns nil for a nil error, and the generated AgentOutput.Error and SessionSnapshot.Error fields are nil whenever nothing failed. Assigning one of those to an error variable produces an interface that is non-nil but holds a nil pointer, and without these guards the first errors.Is or transport call on it would panic, typically inside a request handler.

Field access cannot be guarded the same way: e.Status on a nil *Error panics like any other nil dereference. Read fields only after checking for nil, or go through Of and PublicMessage, which handle it.

func Convert

func Convert(err error) *Error

Convert returns err as an Error, converting it if it is not one already. The converted error takes its status from Of and is never public. Returns nil for a nil err, and also for an err that is itself a non-nil interface holding a nil *Error, so callers must check the result rather than assume it is non-nil. A typed-nil *Error inside a larger chain is skipped instead: the chain is a real error and converts like any other.

Prefer errors.As when you need to know whether err really is an Error; this is for boundaries that must produce one either way.

func Errorf

func Errorf(sentinel *Sentinel, format string, args ...any) *Error

Errorf returns an Error classified by sentinel, with a message built as by fmt.Errorf. Use %w in format to record a cause: the cause stays reachable through errors.Is and errors.As alongside the sentinel.

return status.Errorf(status.ErrNotFound, "model %q not found", name)
return status.Errorf(ai.ErrToolFailed, "tool %q: %w", tool, err)

A nil sentinel is treated as ErrInternal.

Example
package main

import (
	"errors"
	"fmt"

	"github.com/firebase/genkit/go/core/status"
)

// A package declares its domain failure modes from a base sentinel, so callers
// can match at either granularity.
var errMaxTurnsExceeded = status.ErrAborted.Subtype("max turns exceeded")

func main() {
	err := status.Errorf(errMaxTurnsExceeded, "stopped after %d turns", 5)

	fmt.Println(err)
	fmt.Println("specific:", errors.Is(err, errMaxTurnsExceeded))
	fmt.Println("broad:   ", errors.Is(err, status.ErrAborted))
	fmt.Println("status:  ", status.Of(err))
}
Output:
stopped after 5 turns
specific: true
broad:    true
status:   ABORTED
Example (Reclassify)

A tool's own NOT_FOUND is not a NOT_FOUND for the request that invoked it. Reclassify deliberately at the boundary; the original stays reachable.

package main

import (
	"errors"
	"fmt"

	"github.com/firebase/genkit/go/core/status"
)

func main() {
	inner := status.Errorf(status.ErrNotFound, "no row for id 42")
	err := status.Errorf(status.ErrInternal, "tool %q: %w", "lookup", inner)

	fmt.Println(err)
	fmt.Println("status:", status.Of(err))
	fmt.Println("cause still reachable:", errors.Is(err, status.ErrNotFound))
}
Output:
tool "lookup": no row for id 42
status: INTERNAL
cause still reachable: true

func PublicErrorf

func PublicErrorf(sentinel *Sentinel, format string, args ...any) *Error

PublicErrorf is Errorf for a message that is safe to return to clients. Transports may surface the message verbatim, so it must not contain internal details. Everything else is a generic message and the status code alone.

func (*Error) Error

func (e *Error) Error() string

Error implements error. It returns Message alone: the sentinel is a classification label, not a message prefix, so callers control the wording. A nil *Error renders as "<nil>", matching how fmt prints a nil error, rather than as "" which would be indistinguishable from an empty message.

func (*Error) Is

func (e *Error) Is(target error) bool

Is reports whether e was classified by target or by a sentinel derived from it. errors.Is consults this before walking Error.Unwrap, so both granularities match:

errors.Is(err, ai.ErrMaxTurnsExceeded) // the specific sentinel
errors.Is(err, status.ErrAborted)      // the base it derives from

func (Error) JSONSchema

func (Error) JSONSchema() *jsonschema.Schema

JSONSchema describes the error's wire format for schema inference. Without it, inference would reflect over the struct fields, requiring in-process fields that MarshalJSON never emits, so values embedding an Error would fail validation against their own inferred schema.

func (*Error) MarshalJSON

func (e *Error) MarshalJSON() ([]byte, error)

MarshalJSON encodes e in the canonical Genkit error wire format ({status, message, details}). The wire shape ([errorWire]) is generated from the shared JSON schema's RuntimeError definition.

A captured stack is in-process diagnostics, not wire data, so errors embedded in values (a failed agent invocation's output, say) do not leak process internals to clients. Consumers that want the stack read Error.Stack directly. Error.Stack keeps it off Details to begin with; a "stack" entry put there by hand (as the deprecated core.NewError does for compatibility) is dropped here too.

func (*Error) Sentinel

func (e *Error) Sentinel() *Sentinel

Sentinel returns the sentinel that classified e, or nil if it was decoded from the wire rather than constructed in this process.

func (*Error) Stack

func (e *Error) Stack() string

Stack returns the call stack captured when e was constructed, formatted like a panic trace, or "" for an error decoded from the wire. It is formatted on demand: construction only records program counters.

func (*Error) UnmarshalJSON

func (e *Error) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes an Error from the canonical wire format. The result carries no sentinel, cause, or stack: those do not cross the wire.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap returns the cause recorded via %w or Error.WithCause, or nil. The classifying sentinel is deliberately not part of the unwrap chain, so errors.Unwrap and hand-rolled chain walks behave the way they do for any fmt.Errorf result; Error.Is handles sentinel matching.

func (*Error) WithCause

func (e *Error) WithCause(err error) *Error

WithCause records err as e's cause without folding it into the message, and returns e. Use it when the cause is worth keeping reachable through errors.Is and errors.As but not worth repeating in the text:

return status.Errorf(ai.ErrToolFailed, "tool %q failed", name).WithCause(err)

Prefer %w in the format string when the cause belongs in the message. A nil err, or a second call, is a no-op.

func (*Error) WithDetails

func (e *Error) WithDetails(details map[string]any) *Error

WithDetails attaches structured details and returns e, for chaining onto a constructor. Details are serialized and reach clients, so keep them free of internal information unless the error is public.

type Name

type Name string

Name is a canonical status name, drawn from the gRPC status codes. It is the value Genkit puts on the wire, shared by the Go, JS, and Python runtimes.

const (
	OK                 Name = "OK"
	Cancelled          Name = "CANCELLED"
	Unknown            Name = "UNKNOWN"
	InvalidArgument    Name = "INVALID_ARGUMENT"
	DeadlineExceeded   Name = "DEADLINE_EXCEEDED"
	NotFound           Name = "NOT_FOUND"
	AlreadyExists      Name = "ALREADY_EXISTS"
	PermissionDenied   Name = "PERMISSION_DENIED"
	Unauthenticated    Name = "UNAUTHENTICATED"
	ResourceExhausted  Name = "RESOURCE_EXHAUSTED"
	FailedPrecondition Name = "FAILED_PRECONDITION"
	Aborted            Name = "ABORTED"
	OutOfRange         Name = "OUT_OF_RANGE"
	Unimplemented      Name = "UNIMPLEMENTED"
	Internal           Name = "INTERNAL"
	Unavailable        Name = "UNAVAILABLE"
	DataLoss           Name = "DATA_LOSS"
)

The canonical status names.

func Classified

func Classified(err error) (Name, bool)

Classified returns err's status and whether anything in its chain actually carries one. It is Of with the one distinction Of cannot make: an unclassified failure and one deliberately classified Internal both report Internal, and only the second is a decision someone made.

Middleware that acts on a status needs that distinction, since the action it takes on an unclassified error (a network blip from a provider SDK, say) should not follow from INTERNAL happening to be in a configured list:

if s, ok := status.Classified(err); ok && slices.Contains(retryOn, s) { ... }

Cancellation and deadline expiry count as classified. Classified(nil) is (OK, false).

func FromCode

func FromCode(code int) Name

FromCode returns the canonical status name for a gRPC integer code, or Unknown if the code is not canonical. It is the reverse of Name.Code.

Like FromHTTPCode, it is intended for plugins translating a provider's error into a status middleware can reason about. Google APIs report a failed long-running operation as a google.rpc.Status, whose code is numeric.

func FromHTTPCode

func FromHTTPCode(code int) Name

FromHTTPCode returns the canonical status name for an HTTP status code, following the gRPC / Google API reverse mapping. Any 5xx code with no explicit entry falls through to Internal; unmapped 4xx codes return Unknown.

This is intended for plugins wrapping HTTP-based SDK errors so that status-aware middleware (retry, fallback, ...) can reason about them.

func Of

func Of(err error) Name

Of returns the status of err.

It reports the status of the outermost Error in the chain, so a boundary that deliberately reclassifies with Errorf wins over anything beneath it. A bare Sentinel reports its own status. Context cancellation and deadline errors map to Cancelled and DeadlineExceeded. Anything else is Internal: an unclassified failure is a failure of ours, not of the caller's request.

A typed-nil *Error carries no classification: when err itself is one, Of is OK (nothing failed, the nil merely escaped through an error variable), and when one appears inside a chain it is skipped so it cannot mask the rest of the chain.

Of(nil) is OK.

Example (Wrapping)

Adding context as an error travels up the stack must not reclassify it.

package main

import (
	"errors"
	"fmt"

	"github.com/firebase/genkit/go/core/status"
)

func main() {
	err := error(status.Errorf(status.ErrNotFound, "model %q not found", "gemini"))
	err = fmt.Errorf("agent %q: %w", "planner", err)

	fmt.Println(err)
	fmt.Println("status:", status.Of(err))
	fmt.Println("still not found:", errors.Is(err, status.ErrNotFound))
}
Output:
agent "planner": model "gemini" not found
status: NOT_FOUND
still not found: true

func (Name) Code

func (n Name) Code() int

Code returns the gRPC integer code for n, or 2 (Unknown) if n is not canonical.

func (Name) HTTPCode

func (n Name) HTTPCode() int

HTTPCode returns the HTTP status code for n, or 500 if n is not canonical.

func (Name) IsValid

func (n Name) IsValid() bool

IsValid reports whether n is one of the canonical status names.

type Sentinel

type Sentinel struct {
	// contains filtered or unexported fields
}

Sentinel classifies a failure. It pairs a status name with a short, stable label and is the first argument to Errorf and PublicErrorf.

Sentinels are comparable with errors.Is, which is how callers branch on a failure mode instead of matching on message text:

if errors.Is(err, ai.ErrMaxTurnsExceeded) { ... }

A sentinel created with Sentinel.Subtype inherits its parent's status and also matches the parent under errors.Is, so callers can match at whichever granularity they need.

func Base

func Base(n Name) *Sentinel

Base returns the base sentinel for a status name, or ErrUnknown if the name is not canonical. Use it when the status is only known at runtime, such as a plugin translating a provider's error code:

return status.Errorf(status.Base(status.FromHTTPCode(resp.StatusCode)),
	"%s: %s", provider, body)

func NewSentinel

func NewSentinel(status Name, label string) *Sentinel

NewSentinel returns a base sentinel carrying status. Prefer deriving from an existing sentinel with Sentinel.Subtype; use NewSentinel only when introducing a classification that no existing sentinel covers.

func (*Sentinel) Error

func (s *Sentinel) Error() string

Error implements error so a sentinel can be returned or wrapped directly. A nil *Sentinel renders as "<nil>", matching how fmt prints a nil error, so a typed nil in a chain cannot panic the errors.Is walk that finds it.

func (*Sentinel) Status

func (s *Sentinel) Status() Name

Status returns the status name s carries.

func (*Sentinel) Subtype

func (s *Sentinel) Subtype(label string) *Sentinel

Subtype returns a more specific sentinel that inherits s's status and matches s under errors.Is. It is how packages declare domain failure modes:

var ErrMaxTurnsExceeded = status.ErrAborted.Subtype("max turns exceeded")

errors.Is(err, ErrMaxTurnsExceeded) // specific
errors.Is(err, status.ErrAborted)   // broad

func (*Sentinel) Unwrap

func (s *Sentinel) Unwrap() error

Unwrap returns the sentinel s was derived from, or nil for a base sentinel.

Jump to

Keyboard shortcuts

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