Files
coder/coderd/x/chatd/chatstate/errors.go
T
2026-06-04 18:49:39 +00:00

153 lines
5.9 KiB
Go

package chatstate
import (
"errors"
"fmt"
"golang.org/x/xerrors"
)
// Sentinel errors returned by chatstate transitions and helpers.
// Callers should use errors.Is to test for these.
var (
// ErrTransitionNotAllowed is returned when a transition is applied
// to a chat whose current execution state does not permit it. The
// concrete error returned by transition methods is a
// *TransitionError that wraps this sentinel.
ErrTransitionNotAllowed = xerrors.New("chat state transition not allowed")
// ErrInvalidState is returned when the chat row, queue, and
// archive flag together produce a combination outside the 13
// valid execution states described in the RFC.
ErrInvalidState = xerrors.New("chat is in an invalid execution state")
// ErrQueuedMessageNotFound is returned by queue-targeting
// transitions (delete, promote) when the supplied queued message
// ID does not match a row on the chat.
ErrQueuedMessageNotFound = xerrors.New("queued message not found")
// ErrMessageNotFound is returned by [Tx.EditMessage] when the
// target chat_messages row is missing or belongs to another chat.
ErrMessageNotFound = xerrors.New("chat message not found")
// ErrChatNotFound is returned when a non-create transition is
// applied to a chat row that does not exist (or has been deleted
// since the transition started).
ErrChatNotFound = xerrors.New("chat not found")
// ErrChatNotRoot is returned by family-archive helpers when the
// supplied chat is not a root chat (its parent_chat_id is set).
ErrChatNotRoot = xerrors.New("chat is not a root chat")
// ErrEditedMessageNotUser is returned by [Tx.EditMessage] when the
// targeted chat_messages row exists but its role is not user.
ErrEditedMessageNotUser = xerrors.New("only user messages can be edited")
// ErrMessageQueueFull is returned by queue-appending transitions
// when the per-chat queue cap has been reached. The concrete
// error returned by transitions is a *MessageQueueFullError that
// wraps this sentinel.
ErrMessageQueueFull = xerrors.New("chat message queue is full")
// ErrToolResultDuplicate is returned by [Tx.CompleteRequiresAction]
// when the same tool_call_id appears more than once in the
// submitted results.
ErrToolResultDuplicate = xerrors.New("duplicate tool result")
// ErrToolResultUnexpected is returned by
// [Tx.CompleteRequiresAction] when a submitted tool_call_id does
// not correspond to a pending dynamic tool call.
ErrToolResultUnexpected = xerrors.New("unexpected tool result")
// ErrToolResultMissing is returned by [Tx.CompleteRequiresAction]
// when a pending dynamic tool call has no submitted result.
ErrToolResultMissing = xerrors.New("missing tool result")
// ErrToolResultInvalidJSON is returned by
// [Tx.CompleteRequiresAction] when a submitted tool result output
// is not valid JSON.
ErrToolResultInvalidJSON = xerrors.New("tool result output is not valid JSON")
)
// MessageQueueFullError carries the per-chat queue cap so HTTP
// endpoints can include the cap in their response detail. It wraps
// [ErrMessageQueueFull] so callers can match it with errors.Is.
type MessageQueueFullError struct {
Max int64
}
// Error implements the error interface.
func (e *MessageQueueFullError) Error() string {
return fmt.Sprintf("chat message queue is full (max %d)", e.Max)
}
// Unwrap returns [ErrMessageQueueFull] so callers can match the
// generic sentinel.
func (*MessageQueueFullError) Unwrap() error { return ErrMessageQueueFull }
// ToolResultValidationError carries a structured tool-result
// validation failure. It always wraps a specific sentinel
// (ErrToolResultDuplicate, ErrToolResultMissing,
// ErrToolResultUnexpected, ErrToolResultInvalidJSON) so callers can
// match either the generic sentinel or the specific cause.
type ToolResultValidationError struct {
Cause error
ToolCallID string
}
// Error implements the error interface.
func (e *ToolResultValidationError) Error() string {
if e.ToolCallID != "" {
return fmt.Sprintf("%s: %s", e.Cause.Error(), e.ToolCallID)
}
return e.Cause.Error()
}
// Unwrap returns the specific cause so callers can match it.
func (e *ToolResultValidationError) Unwrap() error { return e.Cause }
// TransitionError carries the structured detail for a rejected
// transition. It always wraps [ErrTransitionNotAllowed] so callers can
// match with errors.Is without losing context. When a specific
// chatstate sentinel is the proximate cause, Cause is set and
// errors.Is will match that sentinel too.
type TransitionError struct {
Transition Transition
From ExecutionState
Reason string
Cause error
}
// Error implements the error interface.
func (e *TransitionError) Error() string {
if e.Reason == "" {
return fmt.Sprintf(
"chat state transition %s not allowed from state %s",
e.Transition, e.From,
)
}
return fmt.Sprintf(
"chat state transition %s not allowed from state %s: %s",
e.Transition, e.From, e.Reason,
)
}
// Unwrap returns the error chain attached to this error. The chain
// always includes [ErrTransitionNotAllowed], and may include a more
// specific cause through errors.Join, so callers can use errors.Is
// without custom matching logic on TransitionError.
func (e *TransitionError) Unwrap() error { return e.Cause }
// newTransitionError constructs a typed TransitionError. Returning the
// pointer type lets callers inspect the structured fields when needed.
func newTransitionError(t Transition, from ExecutionState, reason string) *TransitionError {
return &TransitionError{Transition: t, From: from, Reason: reason, Cause: ErrTransitionNotAllowed}
}
// newTransitionErrorWithCause constructs a TransitionError carrying
// a specific underlying sentinel so callers can match the cause with
// errors.Is.
func newTransitionErrorWithCause(t Transition, from ExecutionState, cause error, reason string) *TransitionError {
return &TransitionError{Transition: t, From: from, Reason: reason, Cause: errors.Join(ErrTransitionNotAllowed, cause)}
}