// Package engine assembles Promptkit's internal execution components. package engine import ( "context" "fmt" "io/fs" "net/http" "os" "path/filepath" "strings" "time" "gitea.maximumdirect.net/eric/ConfigKit" "gitea.maximumdirect.net/eric/promptkit/internal/artifact" "gitea.maximumdirect.net/eric/promptkit/internal/capacity" "gitea.maximumdirect.net/eric/promptkit/internal/catalog" "gitea.maximumdirect.net/eric/promptkit/internal/configuration" "gitea.maximumdirect.net/eric/promptkit/internal/defaults" "gitea.maximumdirect.net/eric/promptkit/internal/domain" "gitea.maximumdirect.net/eric/promptkit/internal/generation" "gitea.maximumdirect.net/eric/promptkit/internal/llm" "gitea.maximumdirect.net/eric/promptkit/internal/prompt" "gitea.maximumdirect.net/eric/promptkit/internal/usecase" "gitea.maximumdirect.net/eric/promptkit/internal/validate" ) // Config carries construction settings selected by the public facade. type Config struct { Sources ConfigurationSources SchemaDir string Timeout time.Duration HTTPClient *http.Client TransientRetries *int } // ConfigurationSources identifies caller-selected ConfigKit layers. type ConfigurationSources struct { Underlays []configkit.Underlay Main *configkit.Input } // Options collects construction choices from the public facade. type Options struct { llmClient llm.Client artifactReader artifact.Reader validator validate.Validator validatorSource bool artifactSource bool } // SetLLMClient selects the client collaborator for one engine. func (o *Options) SetLLMClient(client llm.Client) { o.llmClient = client } // SetArtifactReader selects the artifact collaborator for one engine. func (o *Options) SetArtifactReader(reader artifact.Reader) { o.artifactReader = reader o.artifactSource = true } // SetSchemaFS selects a schema source rooted in fsys. func (o *Options) SetSchemaFS(fsys fs.FS, root string) error { if err := validateSourceFS(fsys, root); err != nil { return err } o.validator = validate.NewFSValidator(fsys, root) o.validatorSource = true return nil } // SetSchemaFile selects a single schema file. func (o *Options) SetSchemaFile(path string) error { fsys, root, _, err := fileSource(path) if err != nil { return err } o.validator = validate.NewFSValidator(fsys, root) o.validatorSource = true return nil } // Engine forwards domain operations to the single assembled use-case runner. type Engine struct { runner *usecase.Runner smokeTester *usecase.SmokeTester inspection configuration.Inspection listing identityInventory } type identityInventory interface { ListProfiles(context.Context) ([]string, error) ListPrompts(context.Context) ([]domain.PromptIdentity, error) } // New assembles an engine from facade-selected configuration and collaborators. func New(cfg Config, options Options) (*Engine, error) { defaultRetries, err := domain.ResolveTransientRetries(2, cfg.TransientRetries) if err != nil { return nil, err } underlays, err := catalog.Underlays() if err != nil { return nil, fmt.Errorf("select maintained configuration: %w", err) } underlays = append(underlays, cfg.Sources.Underlays...) main := configkit.Input{Source: configkit.NoSource()} if cfg.Sources.Main != nil { main = *cfg.Sources.Main } snapshot, err := configuration.Load(main, underlays) if err != nil { return nil, err } manager, err := capacity.NewManager(snapshot.CapacityPolicies()) if err != nil { return nil, fmt.Errorf("failed to construct backend capacity manager: %w", err) } validator := options.validator if !options.validatorSource { schemaDir := cfg.SchemaDir if strings.TrimSpace(schemaDir) == "" { schemaDir = defaults.SchemaDirDefault } validator = validate.NewStandardValidator(schemaDir) } var client llm.Client var diagnosticHTTPClient *llm.OpenAICompatibleClient if options.llmClient == nil { builtIn, newErr := llm.NewOpenAICompatibleClient(llm.OpenAICompatibleConfig{Timeout: cfg.Timeout, HTTPClient: cfg.HTTPClient}) if newErr != nil { return nil, newErr } diagnosticHTTPClient = builtIn client = generation.NewBuiltIn(manager, builtIn) } else { client = generation.NewInjected(manager, options.llmClient) } reader := options.artifactReader if !options.artifactSource { reader = artifact.NewCompositeReader() } runner := usecase.NewRunnerWithRepairer(snapshot, snapshot, reader, prompt.NewCompiledRenderer(snapshot), client, validator, usecase.NewDefaultOutputRepairer(client), manager, defaultRetries) return &Engine{ runner: runner, smokeTester: usecase.NewSmokeTester(runner, diagnosticHTTPClient), inspection: snapshot.Inspection(), listing: snapshot, }, nil } // InspectConfiguration returns read-only operations over the captured result. func (e *Engine) InspectConfiguration() configuration.Inspection { if e == nil { return nil } return e.inspection } func validateSourceFS(fsys fs.FS, root string) error { if fsys == nil { return fmt.Errorf("source filesystem is nil") } if strings.TrimSpace(root) == "" { return fmt.Errorf("source root is required") } return nil } func fileSource(name string) (fs.FS, string, string, error) { if strings.TrimSpace(name) == "" { return nil, "", "", fmt.Errorf("source path is required") } directory := filepath.Dir(name) base := filepath.Base(name) if base == "." || base == string(filepath.Separator) { return nil, "", "", fmt.Errorf("source path must name a file") } info, err := os.Stat(name) if err != nil { return nil, "", "", fmt.Errorf("failed to access source file %q: %w", name, err) } if info.IsDir() { return nil, "", "", fmt.Errorf("source path %q must be a file", name) } return os.DirFS(directory), filepath.ToSlash(base), directory, nil } func (e *Engine) InspectPrompt(ctx context.Context, id, version string) (*domain.PromptInspection, error) { return e.runner.InspectPrompt(ctx, id, version) } func (e *Engine) InspectRequirements( ctx context.Context, request usecase.RequirementInspectionRequest, ) (*domain.RequirementInspection, error) { return e.runner.InspectRequirements(ctx, request) } func (e *Engine) InspectProfile(ctx context.Context, id string) (*domain.ProfileInspection, error) { return e.runner.InspectProfile(ctx, id) } func (e *Engine) ListProfiles(ctx context.Context) ([]string, error) { return e.listing.ListProfiles(ctx) } func (e *Engine) ListPrompts(ctx context.Context) ([]domain.PromptIdentity, error) { return e.listing.ListPrompts(ctx) } func (e *Engine) Prepare(ctx context.Context, request domain.RunRequest) (*domain.PreparedRun, error) { return e.runner.Prepare(ctx, request) } func (e *Engine) PrepareExecution(ctx context.Context, request domain.RunRequest) (*usecase.PreparedExecution, error) { return e.runner.PrepareExecution(ctx, request) } func (e *Engine) Run(ctx context.Context, request domain.RunRequest) (*domain.RunResult, error) { return e.runner.Run(ctx, request) } func (e *Engine) RunPrepared(ctx context.Context, prepared *usecase.PreparedExecution) (*domain.RunResult, error) { return e.runner.RunPrepared(ctx, prepared) } func (e *Engine) SmokeTest(ctx context.Context, request domain.SmokeTestRequest) (*domain.SmokeTestReport, error) { if e == nil || e.smokeTester == nil { return nil, fmt.Errorf("%w: smoke tester is not configured", usecase.ErrInvalidRequest) } return e.smokeTester.Run(ctx, request) }