Define processing outcomes and failure contracts
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/outcomes"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/responseschema"
|
||||
)
|
||||
|
||||
@@ -40,15 +41,16 @@ type PromptMetadata struct {
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Content json.RawMessage `json:"content,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
PromptTokens int `json:"prompt_tokens,omitempty"`
|
||||
CompletionTokens int `json:"completion_tokens,omitempty"`
|
||||
TotalTokens int `json:"total_tokens,omitempty"`
|
||||
Prompt PromptMetadata `json:"prompt"`
|
||||
Prepared *PreparedSnapshot `json:"prepared,omitempty"`
|
||||
FinalContent string `json:"final_content,omitempty"`
|
||||
Execution outcomes.ExecutionCounts `json:"execution"`
|
||||
Content json.RawMessage `json:"content,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
PromptTokens int `json:"prompt_tokens,omitempty"`
|
||||
CompletionTokens int `json:"completion_tokens,omitempty"`
|
||||
TotalTokens int `json:"total_tokens,omitempty"`
|
||||
Prompt PromptMetadata `json:"prompt"`
|
||||
Prepared *PreparedSnapshot `json:"prepared,omitempty"`
|
||||
FinalContent string `json:"final_content,omitempty"`
|
||||
}
|
||||
|
||||
// PreparedSnapshot is the exact rendered request snapshot. Its messages may
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package structuredoutput
|
||||
|
||||
import "errors"
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/outcomes"
|
||||
)
|
||||
|
||||
// FailureKind separates invalid generated content from operational failures.
|
||||
type FailureKind string
|
||||
@@ -13,14 +17,31 @@ const (
|
||||
// Failure exposes a safe message while retaining the typed cause for callers.
|
||||
// The cause must not be serialized or formatted directly in user artifacts.
|
||||
type Failure struct {
|
||||
Kind FailureKind
|
||||
Kind FailureKind // Retained until the adapter switches to reason-based classification.
|
||||
Class FailureClass
|
||||
Reason outcomes.ReasonCode
|
||||
Message string
|
||||
Cause error
|
||||
}
|
||||
|
||||
// FailureClass determines whether domain processing may safely skip a model
|
||||
// operation or must stop the run. Parent cancellation always stops the run.
|
||||
type FailureClass string
|
||||
|
||||
const (
|
||||
FailureRecoverable FailureClass = "recoverable"
|
||||
FailureFatal FailureClass = "fatal"
|
||||
FailureCanceled FailureClass = "canceled"
|
||||
)
|
||||
|
||||
func (f *Failure) Error() string { return f.Message }
|
||||
func (f *Failure) Unwrap() error { return f.Cause }
|
||||
|
||||
func IsRecoverable(err error) bool {
|
||||
var failure *Failure
|
||||
return errors.As(err, &failure) && failure.Class == FailureRecoverable
|
||||
}
|
||||
|
||||
func IsMalformedError(err error) bool {
|
||||
var failure *Failure
|
||||
return errors.As(err, &failure) && failure.Kind == FailureMalformedOutput
|
||||
|
||||
@@ -3,6 +3,8 @@ package structuredoutput
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/outcomes"
|
||||
)
|
||||
|
||||
func TestFailureClassificationKeepsCauseAndSafeMessage(t *testing.T) {
|
||||
@@ -16,3 +18,28 @@ func TestFailureClassificationKeepsCauseAndSafeMessage(t *testing.T) {
|
||||
t.Fatalf("runtime classification: %v", runtime)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecoverableFailureKeepsTypedCauseOutOfSafeMessage(t *testing.T) {
|
||||
cause := errors.New("provider response contained a secret")
|
||||
failure := &Failure{
|
||||
Class: FailureRecoverable,
|
||||
Reason: outcomes.ReasonTransportFailed,
|
||||
Message: "model transport failed",
|
||||
Cause: cause,
|
||||
}
|
||||
wrapped := errors.Join(errors.New("operation failed"), failure)
|
||||
if !IsRecoverable(wrapped) || !errors.Is(wrapped, cause) {
|
||||
t.Fatalf("recoverable failure lost classification or cause: %v", wrapped)
|
||||
}
|
||||
if failure.Error() != "model transport failed" {
|
||||
t.Fatalf("unsafe failure text: %q", failure.Error())
|
||||
}
|
||||
for _, class := range []FailureClass{FailureFatal, FailureCanceled} {
|
||||
if IsRecoverable(&Failure{Class: class, Cause: cause}) {
|
||||
t.Fatalf("%s classified as recoverable", class)
|
||||
}
|
||||
}
|
||||
if IsRecoverable(cause) {
|
||||
t.Fatal("bare internal error classified as recoverable")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user