Capture provider failures in secure debug artifacts
This commit is contained in:
@@ -2,6 +2,7 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
@@ -95,6 +96,18 @@ func executePreparedProfile(ctx context.Context, req profileExecutionRequest) (p
|
||||
if callbackFailed {
|
||||
return outcome, nil, &profileExecutionError{operation: "execute prompt", err: err, callbackFailure: true}
|
||||
}
|
||||
if req.DebugWriter != nil && req.DebugWriter.Enabled() && req.DebugRef != nil {
|
||||
var generationError *promptexec.GenerationError
|
||||
if errors.As(err, &generationError) {
|
||||
path, debugErr := req.DebugWriter.WriteFailure(*req.DebugRef, generationError)
|
||||
if path != "" {
|
||||
outcome.LLMDebugPath = path
|
||||
}
|
||||
if debugErr != nil {
|
||||
err = errors.Join(err, promptDebugWriteError(debugErr))
|
||||
}
|
||||
}
|
||||
}
|
||||
return outcome, nil, &profileExecutionError{operation: "execute prompt", err: classifiedPromptError("prompt execution failed", err)}
|
||||
}
|
||||
if execution == nil {
|
||||
|
||||
@@ -21,8 +21,9 @@ import (
|
||||
var ErrSecureCaptureUnsupported = errors.New("secure prompt debug capture is unavailable on this platform")
|
||||
|
||||
const (
|
||||
promptPreparationDebugSchemaVersion = "weatherreporter.prompt_preparation_debug.v2"
|
||||
promptExecutionDebugSchemaVersion = "weatherreporter.prompt_execution_debug.v2"
|
||||
promptPreparationDebugSchemaVersion = "weatherreporter.prompt_preparation_debug.v3"
|
||||
promptExecutionDebugSchemaVersion = "weatherreporter.prompt_execution_debug.v3"
|
||||
promptFailureDebugSchemaVersion = "weatherreporter.prompt_failure_debug.v1"
|
||||
debugDirectoryMode = 0o700
|
||||
debugFileMode = 0o600
|
||||
)
|
||||
@@ -53,6 +54,7 @@ type PromptDebugOutput struct {
|
||||
Format string `json:"format"`
|
||||
ValidationMode string `json:"validationMode"`
|
||||
SchemaPath string `json:"schemaPath"`
|
||||
RepairAttempts int `json:"repairAttempts"`
|
||||
}
|
||||
|
||||
// PromptDebugPreparation is the explicit, content-safe mapping of preparation
|
||||
@@ -97,10 +99,28 @@ type PromptDebugUsage struct {
|
||||
// PromptDebugValidation is the completed validation detail retained in the
|
||||
// explicitly enabled debug store.
|
||||
type PromptDebugValidation struct {
|
||||
Status string `json:"status"`
|
||||
Mode string `json:"mode"`
|
||||
SchemaPath string `json:"schemaPath"`
|
||||
Diagnostics []string `json:"diagnostics,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Mode string `json:"mode"`
|
||||
SchemaPath string `json:"schemaPath"`
|
||||
RepairAttempts int `json:"repairAttempts"`
|
||||
Diagnostics []string `json:"diagnostics,omitempty"`
|
||||
}
|
||||
|
||||
// PromptFailureDebugArtifact records provider detail only in explicit debug storage.
|
||||
type PromptFailureDebugArtifact struct {
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
ReportID report.ID `json:"reportId"`
|
||||
ValidDate string `json:"validDate"`
|
||||
RunID string `json:"runId"`
|
||||
Failure PromptFailure `json:"failure"`
|
||||
}
|
||||
|
||||
type PromptFailure struct {
|
||||
Category string `json:"category"`
|
||||
StatusCode int `json:"statusCode,omitempty"`
|
||||
ProviderCode string `json:"providerCode,omitempty"`
|
||||
ProviderType string `json:"providerType,omitempty"`
|
||||
ProviderMessage string `json:"providerMessage,omitempty"`
|
||||
}
|
||||
|
||||
// PromptDebugExecution is the explicit mapping of execution provenance.
|
||||
@@ -235,6 +255,24 @@ func (w *PromptDebugWriter) WriteExecution(ref PromptDebugRef, execution prompte
|
||||
return directory, nil
|
||||
}
|
||||
|
||||
// WriteFailure stores the project-owned structured provider failure.
|
||||
func (w *PromptDebugWriter) WriteFailure(ref PromptDebugRef, failure *promptexec.GenerationError) (string, error) {
|
||||
if !w.Enabled() || failure == nil {
|
||||
return "", nil
|
||||
}
|
||||
directory, secureDirectory, err := w.runDirectory(ref)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer secureDirectory.Close()
|
||||
artifact := PromptFailureDebugArtifact{SchemaVersion: promptFailureDebugSchemaVersion, ReportID: ref.ReportID, ValidDate: ref.ValidDate, RunID: ref.RunID,
|
||||
Failure: PromptFailure{Category: string(failure.Category()), StatusCode: failure.StatusCode(), ProviderCode: failure.ProviderCode(), ProviderType: failure.ProviderType(), ProviderMessage: failure.ProviderMessage()}}
|
||||
if err := secureDirectory.writeJSON("failure.json", artifact); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return directory, nil
|
||||
}
|
||||
|
||||
func (w *PromptDebugWriter) runDirectory(ref PromptDebugRef) (string, *secureDirectory, error) {
|
||||
if err := validatePromptDebugRef(ref); err != nil {
|
||||
return "", nil, err
|
||||
@@ -283,7 +321,7 @@ func promptDebugPreparation(value promptexec.Preparation) PromptDebugPreparation
|
||||
PromptID: value.PromptID, PromptVersion: value.PromptVersion, PromptHash: value.PromptHash,
|
||||
RenderedPromptHash: value.RenderedPromptHash, InputHashes: copyPromptDebugMap(value.InputHashes),
|
||||
ProfileID: value.ProfileID, BackendID: value.BackendID, ModelName: value.ModelName,
|
||||
Output: PromptDebugOutput{Format: value.Output.Format, ValidationMode: value.Output.ValidationMode, SchemaPath: value.Output.SchemaPath},
|
||||
Output: PromptDebugOutput{Format: value.Output.Format, ValidationMode: value.Output.ValidationMode, SchemaPath: value.Output.SchemaPath, RepairAttempts: value.Output.RepairAttempts},
|
||||
StartedAt: value.StartedAt, EndedAt: value.EndedAt, Duration: value.Duration,
|
||||
}
|
||||
}
|
||||
@@ -300,7 +338,7 @@ func promptDebugExecution(value promptexec.Execution) PromptDebugExecution {
|
||||
}
|
||||
|
||||
func promptDebugValidation(value promptexec.Validation) PromptDebugValidation {
|
||||
return PromptDebugValidation{Status: string(value.Status), Mode: value.Mode, SchemaPath: value.SchemaPath, Diagnostics: append([]string(nil), value.Diagnostics...)}
|
||||
return PromptDebugValidation{Status: string(value.Status), Mode: value.Mode, SchemaPath: value.SchemaPath, RepairAttempts: value.RepairAttempts, Diagnostics: append([]string(nil), value.Diagnostics...)}
|
||||
}
|
||||
|
||||
func promptDebugMessages(values []promptexec.RenderedMessage) []PromptDebugMessage {
|
||||
|
||||
@@ -44,13 +44,13 @@ func TestPromptDebugWriterWritesIsolatedArtifacts(t *testing.T) {
|
||||
t.Fatalf("WriteExecution() directory = %q, want %q", executionDir, preparationDir)
|
||||
}
|
||||
preparationData := readPromptDebugFile(t, filepath.Join(preparationDir, "preparation.json"))
|
||||
for _, want := range []string{"weatherreporter.prompt_preparation_debug.v2", "Use the supplied weather facts.", `"type": "object"`, "https://llm.example.test", `"temperature": 0.2`} {
|
||||
for _, want := range []string{"weatherreporter.prompt_preparation_debug.v3", "Use the supplied weather facts.", `"type": "object"`, "https://llm.example.test", `"temperature": 0.2`, `"repairAttempts": 0`} {
|
||||
if !strings.Contains(string(preparationData), want) {
|
||||
t.Fatalf("preparation debug artifact missing %q:\n%s", want, preparationData)
|
||||
}
|
||||
}
|
||||
executionData := readPromptDebugFile(t, filepath.Join(executionDir, "execution.json"))
|
||||
for _, want := range []string{"weatherreporter.prompt_execution_debug.v2", "Generated forecast prose.", "validation details", `"status": "passed"`} {
|
||||
for _, want := range []string{"weatherreporter.prompt_execution_debug.v3", "Generated forecast prose.", "validation details", `"status": "passed"`, `"repairAttempts": 0`} {
|
||||
if !strings.Contains(string(executionData), want) {
|
||||
t.Fatalf("execution debug artifact missing %q:\n%s", want, executionData)
|
||||
}
|
||||
@@ -66,6 +66,25 @@ func TestPromptDebugWriterWritesIsolatedArtifacts(t *testing.T) {
|
||||
assertPromptDebugMode(t, filepath.Join(executionDir, "execution.json"), debugFileMode)
|
||||
}
|
||||
|
||||
func TestPromptDebugWriterWritesProviderFailureOnlyToDebugStore(t *testing.T) {
|
||||
writer, err := NewPromptDebugWriter(filepath.Join(t.TempDir(), "operator-debug"))
|
||||
if err != nil {
|
||||
t.Fatalf("NewPromptDebugWriter() error = %v", err)
|
||||
}
|
||||
marker := "provider-private-marker"
|
||||
directory, err := writer.WriteFailure(promptDebugRef(), promptexec.NewGenerationError(429, "rate_limit", "provider_error", marker, nil))
|
||||
if err != nil {
|
||||
t.Fatalf("WriteFailure() error = %v", err)
|
||||
}
|
||||
data := readPromptDebugFile(t, filepath.Join(directory, "failure.json"))
|
||||
for _, want := range []string{"weatherreporter.prompt_failure_debug.v1", `"category": "generation"`, `"statusCode": 429`, marker} {
|
||||
if !strings.Contains(string(data), want) {
|
||||
t.Fatalf("failure artifact missing %q: %s", want, data)
|
||||
}
|
||||
}
|
||||
assertPromptDebugMode(t, filepath.Join(directory, "failure.json"), debugFileMode)
|
||||
}
|
||||
|
||||
func TestPromptDebugWriterProjectsProviderConfigurationSafely(t *testing.T) {
|
||||
const marker = "private-debug-marker"
|
||||
tests := []struct {
|
||||
|
||||
Reference in New Issue
Block a user