Files
weatherreporter/internal/promptexec/promptexec.go

278 lines
8.3 KiB
Go

// 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.
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
}
// ProfileInspection describes the safe, selected execution identity for one profile.
type ProfileInspection struct {
ProfileID string
BackendID string
ModelName string
CredentialRequired bool
}
// ExecuteRequest selects one exact prompt execution. DataPackage is the exact
// YAML input; implementations must copy it before retaining it. DataPackagePath
// is provenance for the inline input, not a provider-readable file reference.
type ExecuteRequest struct {
PromptID string
PromptVersion string
ProfileID string
DataPackage []byte
DataPackagePath string
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
DataPackagePath string
}
// 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
DataPackagePath string
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
Diagnostics []string
}
// NewValidation returns a completed validation value with bounded diagnostics.
func NewValidation(status ValidationStatus, mode string, schemaPath string, diagnostics []string) Validation {
return Validation{
Status: status,
Mode: mode,
SchemaPath: schemaPath,
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
}
// 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 ""
}