Add durable prompt execution state records
This commit is contained in:
191
internal/state/prompt_artifacts.go
Normal file
191
internal/state/prompt_artifacts.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptexec"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||
)
|
||||
|
||||
const (
|
||||
PromptPreparationSchemaVersion = "weatherreporter.prompt_preparation.v1"
|
||||
PromptExecutionSchemaVersion = "weatherreporter.prompt_execution.v1"
|
||||
promptArtifactErrorLimit = 2048
|
||||
)
|
||||
|
||||
type PromptPreparationStatus string
|
||||
|
||||
const (
|
||||
PromptPreparationSucceeded PromptPreparationStatus = "succeeded"
|
||||
PromptPreparationFailed PromptPreparationStatus = "failed"
|
||||
)
|
||||
|
||||
// PromptArtifactError is the bounded, classified failure detail retained with a
|
||||
// prompt execution artifact. It deliberately excludes provider error bodies.
|
||||
type PromptArtifactError struct {
|
||||
Category promptexec.ErrorCategory `json:"category"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// NewPromptArtifactError converts an execution error into bounded, durable
|
||||
// diagnostic information without retaining its underlying cause.
|
||||
func NewPromptArtifactError(err error) *PromptArtifactError {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
message := strings.ToValidUTF8(err.Error(), "<22>")
|
||||
if len(message) > promptArtifactErrorLimit {
|
||||
message = message[:promptArtifactErrorLimit]
|
||||
for !utf8.ValidString(message) {
|
||||
message = message[:len(message)-1]
|
||||
}
|
||||
}
|
||||
return &PromptArtifactError{Category: promptexec.CategoryOf(err), Message: message}
|
||||
}
|
||||
|
||||
// PromptPreparationArtifact records the safe provenance available before a
|
||||
// provider is invoked. Preparation debug payloads are never stored here.
|
||||
type PromptPreparationArtifact struct {
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
Status PromptPreparationStatus `json:"status"`
|
||||
ReportID report.ID `json:"reportId"`
|
||||
RunID string `json:"runId"`
|
||||
PromptID string `json:"promptId"`
|
||||
PromptVersion string `json:"promptVersion,omitempty"`
|
||||
DataPackagePath string `json:"dataPackagePath"`
|
||||
Preparation *promptexec.Preparation `json:"preparation,omitempty"`
|
||||
StartedAt time.Time `json:"startedAt"`
|
||||
EndedAt time.Time `json:"endedAt"`
|
||||
Duration time.Duration `json:"duration"`
|
||||
Error *PromptArtifactError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (a PromptPreparationArtifact) Validate() error {
|
||||
if a.SchemaVersion != PromptPreparationSchemaVersion {
|
||||
return fmt.Errorf("unsupported prompt preparation schema version %q", a.SchemaVersion)
|
||||
}
|
||||
if strings.TrimSpace(a.RunID) == "" || a.ReportID == "" || strings.TrimSpace(a.PromptID) == "" {
|
||||
return fmt.Errorf("prompt preparation identity is required")
|
||||
}
|
||||
if strings.TrimSpace(a.DataPackagePath) == "" {
|
||||
return fmt.Errorf("prompt preparation data package path is required")
|
||||
}
|
||||
switch a.Status {
|
||||
case PromptPreparationSucceeded:
|
||||
if a.Preparation == nil || a.Error != nil {
|
||||
return fmt.Errorf("successful prompt preparation requires preparation without an error")
|
||||
}
|
||||
case PromptPreparationFailed:
|
||||
if !validPromptArtifactError(a.Error) {
|
||||
return fmt.Errorf("failed prompt preparation requires a classified error")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported prompt preparation status %q", a.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type PromptExecutionStatus string
|
||||
|
||||
const (
|
||||
PromptExecutionSucceeded PromptExecutionStatus = "succeeded"
|
||||
PromptExecutionValidationRejected PromptExecutionStatus = "validation_rejected"
|
||||
PromptExecutionFailed PromptExecutionStatus = "failed"
|
||||
)
|
||||
|
||||
// PromptExecutionProvenance is the safe subset of promptexec.Execution. The
|
||||
// generated content and debug payload are intentionally excluded.
|
||||
type PromptExecutionProvenance struct {
|
||||
RunID string `json:"runId"`
|
||||
PromptID string `json:"promptId"`
|
||||
PromptVersion string `json:"promptVersion"`
|
||||
PromptHash string `json:"promptHash"`
|
||||
RenderedPromptHash string `json:"renderedPromptHash"`
|
||||
InputHashes map[string]string `json:"inputHashes,omitempty"`
|
||||
ProfileID string `json:"profileId"`
|
||||
BackendID string `json:"backendId"`
|
||||
ModelName string `json:"modelName"`
|
||||
GeneratedHash string `json:"generatedHash,omitempty"`
|
||||
Usage promptexec.TokenUsage `json:"usage"`
|
||||
StartedAt time.Time `json:"startedAt"`
|
||||
EndedAt time.Time `json:"endedAt"`
|
||||
Duration time.Duration `json:"duration"`
|
||||
DataPackagePath string `json:"dataPackagePath"`
|
||||
}
|
||||
|
||||
// PromptExecutionPaths records only destinations reached by a completed run.
|
||||
// It contains paths, never generated content or debug information.
|
||||
type PromptExecutionPaths struct {
|
||||
RawOutputPath string `json:"rawOutputPath,omitempty"`
|
||||
GeneratedTextPath string `json:"generatedTextPath,omitempty"`
|
||||
RenderContextPath string `json:"renderContextPath,omitempty"`
|
||||
RenderedReportPath string `json:"renderedReportPath,omitempty"`
|
||||
OutputPath string `json:"outputPath,omitempty"`
|
||||
NotificationPath string `json:"notificationPath,omitempty"`
|
||||
}
|
||||
|
||||
// PromptExecutionArtifact records safe execution provenance and its validation
|
||||
// outcome. It never embeds generated output or content-rich debug data.
|
||||
type PromptExecutionArtifact struct {
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
Status PromptExecutionStatus `json:"status"`
|
||||
ReportID report.ID `json:"reportId"`
|
||||
RunID string `json:"runId"`
|
||||
PromptID string `json:"promptId"`
|
||||
PromptVersion string `json:"promptVersion,omitempty"`
|
||||
Provenance *PromptExecutionProvenance `json:"provenance,omitempty"`
|
||||
Validation *promptexec.Validation `json:"validation,omitempty"`
|
||||
Paths PromptExecutionPaths `json:"paths,omitempty"`
|
||||
StartedAt time.Time `json:"startedAt"`
|
||||
EndedAt time.Time `json:"endedAt"`
|
||||
Duration time.Duration `json:"duration"`
|
||||
Error *PromptArtifactError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func PromptExecutionProvenanceFrom(value promptexec.Execution) PromptExecutionProvenance {
|
||||
inputHashes := make(map[string]string, len(value.InputHashes))
|
||||
for key, item := range value.InputHashes {
|
||||
inputHashes[key] = item
|
||||
}
|
||||
return PromptExecutionProvenance{
|
||||
RunID: value.RunID, PromptID: value.PromptID, PromptVersion: value.PromptVersion,
|
||||
PromptHash: value.PromptHash, RenderedPromptHash: value.RenderedPromptHash,
|
||||
InputHashes: inputHashes, ProfileID: value.ProfileID, BackendID: value.BackendID,
|
||||
ModelName: value.ModelName, GeneratedHash: value.GeneratedHash, Usage: value.Usage,
|
||||
StartedAt: value.StartedAt, EndedAt: value.EndedAt, Duration: value.Duration,
|
||||
DataPackagePath: value.DataPackagePath,
|
||||
}
|
||||
}
|
||||
|
||||
func (a PromptExecutionArtifact) Validate() error {
|
||||
if a.SchemaVersion != PromptExecutionSchemaVersion {
|
||||
return fmt.Errorf("unsupported prompt execution schema version %q", a.SchemaVersion)
|
||||
}
|
||||
if strings.TrimSpace(a.RunID) == "" || a.ReportID == "" || strings.TrimSpace(a.PromptID) == "" {
|
||||
return fmt.Errorf("prompt execution identity is required")
|
||||
}
|
||||
switch a.Status {
|
||||
case PromptExecutionSucceeded:
|
||||
if a.Provenance == nil || a.Validation == nil || a.Validation.Status != promptexec.ValidationPassed || a.Error != nil {
|
||||
return fmt.Errorf("successful prompt execution requires passed validation without an error")
|
||||
}
|
||||
case PromptExecutionValidationRejected:
|
||||
if a.Provenance == nil || a.Validation == nil || a.Validation.Status != promptexec.ValidationFailed || a.Error != nil {
|
||||
return fmt.Errorf("validation-rejected prompt execution requires failed validation without an error")
|
||||
}
|
||||
case PromptExecutionFailed:
|
||||
if !validPromptArtifactError(a.Error) {
|
||||
return fmt.Errorf("failed prompt execution requires a classified error")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported prompt execution status %q", a.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validPromptArtifactError(value *PromptArtifactError) bool {
|
||||
return value != nil && value.Category != "" && strings.TrimSpace(value.Message) != "" && len(value.Message) <= promptArtifactErrorLimit && utf8.ValidString(value.Message)
|
||||
}
|
||||
Reference in New Issue
Block a user