// Package promptexec defines Weatherreporter's provider-neutral prompt execution contract. package promptexec import ( "context" "errors" "fmt" "time" ) const ( maxValidationDiagnostics = 10 maxDiagnosticBytes = 1024 maxErrorMessageBytes = 2048 ) // Executor inspects and executes configured prompts without exposing provider types. // Inspection is side-effect-free. Execute invokes prepared exactly once after a // successful preparation and before provider execution. If prepared returns an // error, Execute must not call the provider. Completed validation rejection is // returned as an Execution with a failed Validation status; operational failures // return no Execution. Sensitive debug values are populated only when requested. // Comparison may invoke Execute concurrently on one shared Executor, so every // implementation must support concurrent calls. type Executor interface { InspectPrompt(context.Context, string, string) (PromptInspection, error) InspectProfile(context.Context, string) (ProfileInspection, error) Execute(context.Context, ExecuteRequest, PreparationCallback) (*Execution, error) } // PromptInspection describes one exact prompt definition without selecting a profile. type PromptInspection struct { PromptID string PromptVersion string PromptHash string DefaultProfileID string Inputs []InputDefinition Output OutputContract } // InputDefinition describes one declared prompt input. type InputDefinition struct { Name string Required bool ContentType string Description string } // OutputContract summarizes the output requirements declared by a prompt. type OutputContract struct { Format string ValidationMode string SchemaPath string RepairAttempts int } // ProfileInspection describes the safe, selected execution identity for one profile. type ProfileInspection struct { ProfileID string BackendID string ModelName string CredentialRequired bool APIKeyEnv string } // ExecuteRequest selects one exact prompt execution. DataPackage is the exact // YAML input; implementations must copy it before retaining it. type ExecuteRequest struct { PromptID string PromptVersion string ProfileID string DataPackage []byte CaptureDebug bool } // PreparationCallback receives safe preparation provenance before provider work. // The callback receives independent copies which it may retain or mutate. type PreparationCallback func(Preparation, *PreparationDebug) error // Preparation contains non-sensitive provenance from a completed preparation. type Preparation struct { PromptID string PromptVersion string PromptHash string RenderedPromptHash string InputHashes map[string]string ProfileID string BackendID string ModelName string Output OutputContract StartedAt time.Time EndedAt time.Time Duration time.Duration } // PreparationDebug contains content-rich preparation details for an explicitly // enabled sensitive-debug destination. It must never be persisted routinely. type PreparationDebug struct { RenderedMessages []RenderedMessage StructuredSchema []byte Endpoint string ParametersJSON []byte } // RenderedMessage is one fully rendered model message for sensitive debugging. type RenderedMessage struct { Role string Content string } // Execution contains the completed result of one provider run. RawOutput is // the generated content, not a provider transport response body. It is copied // before return and must be persisted separately from routine metadata. type Execution struct { RunID string PromptID string PromptVersion string PromptHash string RenderedPromptHash string InputHashes map[string]string ProfileID string BackendID string ModelName string GeneratedHash string Usage TokenUsage StartedAt time.Time EndedAt time.Time Duration time.Duration Validation Validation RawOutput []byte Debug *ExecutionDebug } // TokenUsage is provider-reported token accounting. type TokenUsage struct { PromptTokens int CompletionTokens int TotalTokens int CachedTokens int CacheWriteTokens int } // Validation records a completed output validation check. type Validation struct { Status ValidationStatus Mode string SchemaPath string RepairAttempts int Diagnostics []string } // NewValidation returns a completed validation value with bounded diagnostics. func NewValidation(status ValidationStatus, mode string, schemaPath string, repairAttempts int, diagnostics []string) Validation { return Validation{ Status: status, Mode: mode, SchemaPath: schemaPath, RepairAttempts: repairAttempts, Diagnostics: boundDiagnostics(diagnostics), } } // ValidationStatus identifies the completed validation state. type ValidationStatus string const ( ValidationPassed ValidationStatus = "passed" ValidationFailed ValidationStatus = "failed" ValidationSkipped ValidationStatus = "skipped" ) // ExecutionDebug contains content-rich execution details for explicitly enabled // sensitive debugging. It must never be persisted routinely. type ExecutionDebug struct { RawOutput []byte ValidationDiagnostics []string } // ErrorCategory classifies a project-owned operational failure. type ErrorCategory string const ( InvalidConfiguration ErrorCategory = "invalid_configuration" InvalidRequest ErrorCategory = "invalid_request" PromptNotFound ErrorCategory = "prompt_not_found" PromptLoad ErrorCategory = "prompt_load" ProfileNotFound ErrorCategory = "profile_not_found" ProfileLoad ErrorCategory = "profile_load" MissingCredential ErrorCategory = "missing_credential" ArtifactLoad ErrorCategory = "artifact_load" PromptRender ErrorCategory = "prompt_render" Capacity ErrorCategory = "capacity" Generation ErrorCategory = "generation" OperationalValidation ErrorCategory = "operational_validation" ValidationRejected ErrorCategory = "validation_rejected" Canceled ErrorCategory = "canceled" DeadlineExceeded ErrorCategory = "deadline_exceeded" ) // Error is a bounded safe error suitable for workflow and persistence records. // Its optional cause remains available to errors.Is and errors.As but is never // included in Error's text. type Error struct { category ErrorCategory message string cause error } // NewError returns a classified error with a bounded, Weatherreporter-owned message. func NewError(category ErrorCategory, message string, cause error) *Error { messageLimit := maxErrorMessageBytes - len(category) - len(": ") return &Error{category: category, message: boundText(message, messageLimit), cause: cause} } func (e *Error) Error() string { if e == nil { return "" } if e.message == "" { return string(e.category) } return fmt.Sprintf("%s: %s", e.category, e.message) } // Unwrap preserves an underlying error identity without exposing its text. func (e *Error) Unwrap() error { if e == nil { return nil } return e.cause } // Category returns the stable classification. func (e *Error) Category() ErrorCategory { if e == nil { return "" } return e.category } // GenerationError retains provider failure details for programmatic handling // without exposing them through routine formatting or serialization. type GenerationError struct { statusCode int providerCode string providerType string providerMessage string err *Error } // NewGenerationError returns a classified generation failure with bounded // provider details. The dependency cause remains reachable through err only. func NewGenerationError(statusCode int, providerCode string, providerType string, providerMessage string, cause error) *GenerationError { return &GenerationError{ statusCode: statusCode, providerCode: boundCodePoints(providerCode, 256), providerType: boundCodePoints(providerType, 256), providerMessage: boundCodePoints(providerMessage, 4096), err: NewError(Generation, "provider generation failed", cause), } } // StatusCode returns the provider HTTP status when one was available. func (e *GenerationError) StatusCode() int { if e == nil { return 0 } return e.statusCode } // ProviderCode returns the bounded provider error code. func (e *GenerationError) ProviderCode() string { if e == nil { return "" } return e.providerCode } // ProviderType returns the bounded provider error type. func (e *GenerationError) ProviderType() string { if e == nil { return "" } return e.providerType } // ProviderMessage returns the bounded provider error message. func (e *GenerationError) ProviderMessage() string { if e == nil { return "" } return e.providerMessage } // Category returns Generation for every generation failure. func (e *GenerationError) Category() ErrorCategory { return Generation } // Error intentionally excludes provider details from ordinary error text. func (e *GenerationError) Error() string { if e == nil { return "" } message := NewError(Generation, "provider generation failed", nil).Error() if e.statusCode == 0 { return message } return fmt.Sprintf("%s (HTTP %d)", message, e.statusCode) } // GoString keeps %#v formatting as safe as ordinary error formatting. func (e *GenerationError) GoString() string { return e.Error() } // Unwrap preserves the project-owned classified error and its hidden cause. func (e *GenerationError) Unwrap() error { if e == nil { return nil } return e.err } // CapacityError adds the safe backend identity to a capacity failure. type CapacityError struct { BackendID string Err *Error } // NewCapacityError returns a classified capacity failure for backendID. func NewCapacityError(backendID string, message string, cause error) *CapacityError { return &CapacityError{BackendID: boundText(backendID, 256), Err: NewError(Capacity, message, cause)} } func (e *CapacityError) Error() string { if e == nil { return "" } if e.BackendID == "" { return e.Err.Error() } return boundText(fmt.Sprintf("%s (backend %q)", e.Err.Error(), e.BackendID), maxErrorMessageBytes) } func (e *CapacityError) Unwrap() error { if e == nil { return nil } return e.Err } // Category returns Capacity for every capacity error. func (e *CapacityError) Category() ErrorCategory { return Capacity } // CategoryOf returns the classification carried by err, including wrapped errors. func CategoryOf(err error) ErrorCategory { var categorized interface{ Category() ErrorCategory } if errors.As(err, &categorized) { return categorized.Category() } return "" }