Track the operation owning internal errors
This commit is contained in:
@@ -120,6 +120,11 @@ Go 1.27.3, including nested capture and deferred unsupported/cyclic errors.
|
|||||||
category and matchable cause. Finish facade behavior in Stage 4; do not add
|
category and matchable cause. Finish facade behavior in Stage 4; do not add
|
||||||
temporary mapping paths just for this intermediate stage.
|
temporary mapping paths just for this intermediate stage.
|
||||||
|
|
||||||
|
Completion: operation errors retain their owning category and original cause,
|
||||||
|
including direct prompt/profile lookup categories. Focused use-case tests pass
|
||||||
|
on Go 1.27.3. Public projection and built-in preparation classification remain
|
||||||
|
for their respective later units.
|
||||||
|
|
||||||
## Stage 4: Preserve Categories Through Public Error Projection
|
## Stage 4: Preserve Categories Through Public Error Projection
|
||||||
|
|
||||||
**Catalog:** 7, completion. **Files:** `errors.go` and public error contracts/tests.
|
**Catalog:** 7, completion. **Files:** `errors.go` and public error contracts/tests.
|
||||||
|
|||||||
38
internal/usecase/operation_error.go
Normal file
38
internal/usecase/operation_error.go
Normal 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}
|
||||||
|
}
|
||||||
34
internal/usecase/operation_error_test.go
Normal file
34
internal/usecase/operation_error_test.go
Normal 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")
|
||||||
@@ -39,7 +39,7 @@ func (r *Runner) resolveProfileSelection(
|
|||||||
}
|
}
|
||||||
selectedProfile, err := r.profiles.GetProfile(ctx, lookupID)
|
selectedProfile, err := r.profiles.GetProfile(ctx, lookupID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: %w", ErrProfileLoad, err)
|
return nil, profileLookupError(err)
|
||||||
}
|
}
|
||||||
if selectedProfile == nil {
|
if selectedProfile == nil {
|
||||||
return nil, fmt.Errorf("%w: profile repository returned nil profile", ErrProfileLoad)
|
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 {
|
if indexed, ok := r.profiles.(profileIdentityRepository); ok {
|
||||||
identity, err := indexed.GetProfileIdentity(ctx, lookupID)
|
identity, err := indexed.GetProfileIdentity(ctx, lookupID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("%w: %w", ErrProfileLoad, err)
|
return "", profileLookupError(err)
|
||||||
}
|
}
|
||||||
return identity, nil
|
return identity, nil
|
||||||
}
|
}
|
||||||
@@ -108,7 +108,7 @@ func (r *Runner) InspectProfile(
|
|||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return nil, fmt.Errorf("%w: %w", ErrProfileLoad, ctx.Err())
|
return nil, &OperationError{Category: ErrProfileLoad, Cause: ctx.Err()}
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ func (r *Runner) InspectProfile(
|
|||||||
target.APIKey = ""
|
target.APIKey = ""
|
||||||
params, err := jsonvalue.CopyMap(target.ExtraParams)
|
params, err := jsonvalue.CopyMap(target.ExtraParams)
|
||||||
if err != nil {
|
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
|
target.ExtraParams = params
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func (r *Runner) resolvePromptDefinition(ctx context.Context, promptID string) (
|
|||||||
definition, err = r.promptDefs.GetPromptDefinition(ctx, promptID)
|
definition, err = r.promptDefs.GetPromptDefinition(ctx, promptID)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: %w", ErrPromptLoad, err)
|
return nil, promptLookupError(err)
|
||||||
}
|
}
|
||||||
if definition == nil {
|
if definition == nil {
|
||||||
return nil, fmt.Errorf("%w: prompt repository returned nil definition", ErrPromptLoad)
|
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 {
|
if indexed, ok := r.promptDefs.(promptInspectionRepository); ok {
|
||||||
inspection, err := indexed.GetPromptInspection(ctx, promptID)
|
inspection, err := indexed.GetPromptInspection(ctx, promptID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: %w", ErrPromptLoad, err)
|
return nil, promptLookupError(err)
|
||||||
}
|
}
|
||||||
if inspection == nil {
|
if inspection == nil {
|
||||||
return nil, fmt.Errorf("%w: prompt repository returned nil inspection", ErrPromptLoad)
|
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 {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return nil, fmt.Errorf("%w: %w", ErrPromptLoad, ctx.Err())
|
return nil, &OperationError{Category: ErrPromptLoad, Cause: ctx.Err()}
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func (r *Runner) InspectRequirements(
|
|||||||
return nil, fmt.Errorf("%w: session_id: %v", ErrInvalidRequest, err)
|
return nil, fmt.Errorf("%w: session_id: %v", ErrInvalidRequest, err)
|
||||||
}
|
}
|
||||||
if err := ctx.Err(); err != nil {
|
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)
|
selection, err := r.resolvePromptInspection(ctx, req.PromptID)
|
||||||
@@ -44,7 +44,7 @@ func (r *Runner) InspectRequirements(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil, fmt.Errorf("%w: %w", ErrProfileLoad, err)
|
return nil, &OperationError{Category: ErrProfileLoad, Cause: err}
|
||||||
}
|
}
|
||||||
selectedProfileID := req.ProfileID
|
selectedProfileID := req.ProfileID
|
||||||
if strings.TrimSpace(selectedProfileID) == "" {
|
if strings.TrimSpace(selectedProfileID) == "" {
|
||||||
@@ -58,7 +58,7 @@ func (r *Runner) InspectRequirements(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := ctx.Err(); err != nil {
|
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)
|
repository, ok := r.promptDefs.(requirementSnapshotRepository)
|
||||||
@@ -72,7 +72,7 @@ func (r *Runner) InspectRequirements(
|
|||||||
definition := &domain.PromptDefinition{ID: selection.PromptID}
|
definition := &domain.PromptDefinition{ID: selection.PromptID}
|
||||||
inspection, err := prompt.AnalyzeRequirements(ctx, definition, snapshot, req.Vars, directSessionID != "")
|
inspection, err := prompt.AnalyzeRequirements(ctx, definition, snapshot, req.Vars, directSessionID != "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: %w", ErrPromptLoad, err)
|
return nil, &OperationError{Category: ErrPromptLoad, Cause: err}
|
||||||
}
|
}
|
||||||
inspection.ProfileID = profileIdentity
|
inspection.ProfileID = profileIdentity
|
||||||
return &inspection, nil
|
return &inspection, nil
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ func (r *Runner) executePreparedRun(
|
|||||||
outputArtifact := buildOutputArtifact(genResp.Content, prepared.OutputContract.Format)
|
outputArtifact := buildOutputArtifact(genResp.Content, prepared.OutputContract.Format)
|
||||||
validationResult, err := validateArtifact(ctx, &outputArtifact, 0)
|
validationResult, err := validateArtifact(ctx, &outputArtifact, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: %w", ErrValidation, err)
|
return nil, &OperationError{Category: ErrValidation, Cause: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.shouldAttemptRepair(prepared.OutputContract, validationResult) {
|
if r.shouldAttemptRepair(prepared.OutputContract, validationResult) {
|
||||||
@@ -238,7 +238,7 @@ func (r *Runner) executePreparedRun(
|
|||||||
|
|
||||||
validationResult, err = validateArtifact(ctx, &outputArtifact, attemptsUsed)
|
validationResult, err = validateArtifact(ctx, &outputArtifact, attemptsUsed)
|
||||||
if err != nil {
|
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 {
|
func wrapGenerationError(err error) error {
|
||||||
if errors.Is(err, llm.ErrInvalidRequest) {
|
return &OperationError{Category: ErrLLMGenerate, Cause: err}
|
||||||
return fmt.Errorf("%w: %w", ErrInvalidRequest, err)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("%w: %w", ErrLLMGenerate, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func addTokenUsage(total, next domain.TokenUsage) domain.TokenUsage {
|
func addTokenUsage(total, next domain.TokenUsage) domain.TokenUsage {
|
||||||
@@ -408,7 +405,7 @@ func (r *Runner) prepareValidation(
|
|||||||
}
|
}
|
||||||
plan, err := preparer.PrepareValidation(ctx, contract)
|
plan, err := preparer.PrepareValidation(ctx, contract)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: %w", ErrValidation, err)
|
return nil, &OperationError{Category: ErrValidation, Cause: err}
|
||||||
}
|
}
|
||||||
if plan == nil {
|
if plan == nil {
|
||||||
return nil, fmt.Errorf("%w: validator returned nil prepared validation", ErrValidation)
|
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 {
|
for name, ref := range req.Inputs {
|
||||||
art, readErr := r.artifacts.Read(ctx, ref)
|
art, readErr := r.artifacts.Read(ctx, ref)
|
||||||
if readErr != nil {
|
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 == "" {
|
if art.Name == "" {
|
||||||
art.Name = name
|
art.Name = name
|
||||||
@@ -467,7 +464,7 @@ func (r *Runner) completePreparationWithStructuredOutput(
|
|||||||
}
|
}
|
||||||
renderedPrompt, err := r.renderer.Render(ctx, definitionToRender, resolvedInputs, req.Vars)
|
renderedPrompt, err := r.renderer.Render(ctx, definitionToRender, resolvedInputs, req.Vars)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: %w", ErrPromptRender, err)
|
return nil, &OperationError{Category: ErrPromptRender, Cause: err}
|
||||||
}
|
}
|
||||||
effectivePrompt := *renderedPrompt
|
effectivePrompt := *renderedPrompt
|
||||||
if state.directSessionID != "" {
|
if state.directSessionID != "" {
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ func (r *Runner) resolveSmokeBackend(id string) (domain.Backend, error) {
|
|||||||
}
|
}
|
||||||
backend, err := lookup.GetBackend(id)
|
backend, err := lookup.GetBackend(id)
|
||||||
if err != nil {
|
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
|
return backend, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user