Track the operation owning internal errors

This commit is contained in:
2026-11-05 13:42:56 +00:00
parent 106c808442
commit e881bbdcf4
8 changed files with 95 additions and 21 deletions

View File

@@ -0,0 +1,38 @@
package usecase
import (
"errors"
"fmt"
"go.maximumdirect.net/promptkit/internal/profile"
"go.maximumdirect.net/promptkit/internal/promptdef"
)
// OperationError identifies the operation that owns a failure while retaining
// the original cause for inspection.
type OperationError struct {
Category error
Cause error
}
func (e *OperationError) Error() string { return fmt.Sprintf("%v: %v", e.Category, e.Cause) }
func (e *OperationError) Unwrap() error { return e.Cause }
func (e *OperationError) Is(target error) bool { return target == e.Category }
func promptLookupError(cause error) error {
category := ErrPromptLoad
if errors.Is(cause, promptdef.ErrPromptDefinitionNotFound) {
category = promptdef.ErrPromptDefinitionNotFound
cause = fmt.Errorf("%w: %w", ErrPromptLoad, cause)
}
return &OperationError{Category: category, Cause: cause}
}
func profileLookupError(cause error) error {
category := ErrProfileLoad
if errors.Is(cause, profile.ErrProfileNotFound) {
category = profile.ErrProfileNotFound
cause = fmt.Errorf("%w: %w", ErrProfileLoad, cause)
}
return &OperationError{Category: category, Cause: cause}
}

View File

@@ -0,0 +1,34 @@
package usecase
import (
"errors"
"fmt"
"testing"
"go.maximumdirect.net/promptkit/internal/profile"
"go.maximumdirect.net/promptkit/internal/promptdef"
)
func TestOperationErrorRetainsOwnerAndCause(t *testing.T) {
cause := fmt.Errorf("nested: %w", promptdef.ErrPromptDefinitionNotFound)
outer := &OperationError{Category: ErrLLMGenerate, Cause: cause}
if !errors.Is(outer, ErrLLMGenerate) || !errors.Is(outer, promptdef.ErrPromptDefinitionNotFound) || errors.Unwrap(outer) != cause {
t.Fatalf("operation error = %v", outer)
}
for _, test := range []struct {
err error
category error
}{
{promptLookupError(cause), promptdef.ErrPromptDefinitionNotFound},
{profileLookupError(profile.ErrProfileNotFound), profile.ErrProfileNotFound},
{promptLookupError(contextFailure), ErrPromptLoad},
{profileLookupError(contextFailure), ErrProfileLoad},
} {
var operation *OperationError
if !errors.As(test.err, &operation) || operation.Category != test.category {
t.Errorf("operation = %+v", operation)
}
}
}
var contextFailure = errors.New("lookup failed")

View File

@@ -39,7 +39,7 @@ func (r *Runner) resolveProfileSelection(
}
selectedProfile, err := r.profiles.GetProfile(ctx, lookupID)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrProfileLoad, err)
return nil, profileLookupError(err)
}
if selectedProfile == nil {
return nil, fmt.Errorf("%w: profile repository returned nil profile", ErrProfileLoad)
@@ -64,7 +64,7 @@ func (r *Runner) resolveProfileIdentity(ctx context.Context, profileID string) (
if indexed, ok := r.profiles.(profileIdentityRepository); ok {
identity, err := indexed.GetProfileIdentity(ctx, lookupID)
if err != nil {
return "", fmt.Errorf("%w: %w", ErrProfileLoad, err)
return "", profileLookupError(err)
}
return identity, nil
}
@@ -108,7 +108,7 @@ func (r *Runner) InspectProfile(
}
select {
case <-ctx.Done():
return nil, fmt.Errorf("%w: %w", ErrProfileLoad, ctx.Err())
return nil, &OperationError{Category: ErrProfileLoad, Cause: ctx.Err()}
default:
}
@@ -120,7 +120,7 @@ func (r *Runner) InspectProfile(
target.APIKey = ""
params, err := jsonvalue.CopyMap(target.ExtraParams)
if err != nil {
return nil, fmt.Errorf("%w: profile extra parameters: %w", ErrProfileLoad, err)
return nil, &OperationError{Category: ErrProfileLoad, Cause: fmt.Errorf("profile extra parameters: %w", err)}
}
target.ExtraParams = params

View File

@@ -36,7 +36,7 @@ func (r *Runner) resolvePromptDefinition(ctx context.Context, promptID string) (
definition, err = r.promptDefs.GetPromptDefinition(ctx, promptID)
}
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrPromptLoad, err)
return nil, promptLookupError(err)
}
if definition == nil {
return nil, fmt.Errorf("%w: prompt repository returned nil definition", ErrPromptLoad)
@@ -61,7 +61,7 @@ func (r *Runner) resolvePromptInspection(ctx context.Context, promptID string) (
if indexed, ok := r.promptDefs.(promptInspectionRepository); ok {
inspection, err := indexed.GetPromptInspection(ctx, promptID)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrPromptLoad, err)
return nil, promptLookupError(err)
}
if inspection == nil {
return nil, fmt.Errorf("%w: prompt repository returned nil inspection", ErrPromptLoad)
@@ -90,7 +90,7 @@ func (r *Runner) InspectPrompt(ctx context.Context, promptID string) (*domain.Pr
}
select {
case <-ctx.Done():
return nil, fmt.Errorf("%w: %w", ErrPromptLoad, ctx.Err())
return nil, &OperationError{Category: ErrPromptLoad, Cause: ctx.Err()}
default:
}

View File

@@ -36,7 +36,7 @@ func (r *Runner) InspectRequirements(
return nil, fmt.Errorf("%w: session_id: %v", ErrInvalidRequest, err)
}
if err := ctx.Err(); err != nil {
return nil, fmt.Errorf("%w: %w", ErrPromptLoad, err)
return nil, &OperationError{Category: ErrPromptLoad, Cause: err}
}
selection, err := r.resolvePromptInspection(ctx, req.PromptID)
@@ -44,7 +44,7 @@ func (r *Runner) InspectRequirements(
return nil, err
}
if err := ctx.Err(); err != nil {
return nil, fmt.Errorf("%w: %w", ErrProfileLoad, err)
return nil, &OperationError{Category: ErrProfileLoad, Cause: err}
}
selectedProfileID := req.ProfileID
if strings.TrimSpace(selectedProfileID) == "" {
@@ -58,7 +58,7 @@ func (r *Runner) InspectRequirements(
return nil, err
}
if err := ctx.Err(); err != nil {
return nil, fmt.Errorf("%w: %w", ErrPromptLoad, err)
return nil, &OperationError{Category: ErrPromptLoad, Cause: err}
}
repository, ok := r.promptDefs.(requirementSnapshotRepository)
@@ -72,7 +72,7 @@ func (r *Runner) InspectRequirements(
definition := &domain.PromptDefinition{ID: selection.PromptID}
inspection, err := prompt.AnalyzeRequirements(ctx, definition, snapshot, req.Vars, directSessionID != "")
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrPromptLoad, err)
return nil, &OperationError{Category: ErrPromptLoad, Cause: err}
}
inspection.ProfileID = profileIdentity
return &inspection, nil

View File

@@ -204,7 +204,7 @@ func (r *Runner) executePreparedRun(
outputArtifact := buildOutputArtifact(genResp.Content, prepared.OutputContract.Format)
validationResult, err := validateArtifact(ctx, &outputArtifact, 0)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrValidation, err)
return nil, &OperationError{Category: ErrValidation, Cause: err}
}
if r.shouldAttemptRepair(prepared.OutputContract, validationResult) {
@@ -238,7 +238,7 @@ func (r *Runner) executePreparedRun(
validationResult, err = validateArtifact(ctx, &outputArtifact, attemptsUsed)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrValidation, err)
return nil, &OperationError{Category: ErrValidation, Cause: err}
}
}
}
@@ -269,10 +269,7 @@ func (r *Runner) executePreparedRun(
}
func wrapGenerationError(err error) error {
if errors.Is(err, llm.ErrInvalidRequest) {
return fmt.Errorf("%w: %w", ErrInvalidRequest, err)
}
return fmt.Errorf("%w: %w", ErrLLMGenerate, err)
return &OperationError{Category: ErrLLMGenerate, Cause: err}
}
func addTokenUsage(total, next domain.TokenUsage) domain.TokenUsage {
@@ -408,7 +405,7 @@ func (r *Runner) prepareValidation(
}
plan, err := preparer.PrepareValidation(ctx, contract)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrValidation, err)
return nil, &OperationError{Category: ErrValidation, Cause: err}
}
if plan == nil {
return nil, fmt.Errorf("%w: validator returned nil prepared validation", ErrValidation)
@@ -450,7 +447,7 @@ func (r *Runner) completePreparationWithStructuredOutput(
for name, ref := range req.Inputs {
art, readErr := r.artifacts.Read(ctx, ref)
if readErr != nil {
return nil, fmt.Errorf("%w: input %q: %w", ErrArtifactLoad, name, readErr)
return nil, &OperationError{Category: ErrArtifactLoad, Cause: fmt.Errorf("input %q: %w", name, readErr)}
}
if art.Name == "" {
art.Name = name
@@ -467,7 +464,7 @@ func (r *Runner) completePreparationWithStructuredOutput(
}
renderedPrompt, err := r.renderer.Render(ctx, definitionToRender, resolvedInputs, req.Vars)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrPromptRender, err)
return nil, &OperationError{Category: ErrPromptRender, Cause: err}
}
effectivePrompt := *renderedPrompt
if state.directSessionID != "" {

View File

@@ -144,7 +144,7 @@ func (r *Runner) resolveSmokeBackend(id string) (domain.Backend, error) {
}
backend, err := lookup.GetBackend(id)
if err != nil {
return domain.Backend{}, fmt.Errorf("%w: backend %q: %v", ErrProfileLoad, id, err)
return domain.Backend{}, &OperationError{Category: ErrProfileLoad, Cause: fmt.Errorf("backend %q: %w", id, err)}
}
return backend, nil
}