// Package engine assembles Promptkit's internal execution components. package engine import ( "context" "fmt" "net/http" "strings" "time" openrouter "gitea.maximumdirect.net/eric/promptkit-backend-openrouter" rakestrawhome "gitea.maximumdirect.net/eric/promptkit-backend-rakestrawhome" "gitea.maximumdirect.net/eric/promptkit/internal/artifact" "gitea.maximumdirect.net/eric/promptkit/internal/backend" "gitea.maximumdirect.net/eric/promptkit/internal/capacity" "gitea.maximumdirect.net/eric/promptkit/internal/catalog" "gitea.maximumdirect.net/eric/promptkit/internal/defaults" "gitea.maximumdirect.net/eric/promptkit/internal/domain" "gitea.maximumdirect.net/eric/promptkit/internal/llm" "gitea.maximumdirect.net/eric/promptkit/internal/profile" "gitea.maximumdirect.net/eric/promptkit/internal/prompt" "gitea.maximumdirect.net/eric/promptkit/internal/promptdef" "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 { PromptDir string ProfileDir string SchemaDir string Timeout time.Duration HTTPClient *http.Client } // Options carries collaborators prepared by the public facade during the // transitional composition split. type Options struct { LLMClient llm.Client ArtifactReader artifact.Reader PromptDefinitions promptdef.Repository Profiles profile.Repository FallbackProfiles profile.Repository MemoryProfiles profile.Repository Backends []domain.Backend Validator validate.Validator PromptSource bool ProfileSource bool FallbackProfileSource bool MemorySource bool ValidatorSource bool ArtifactSource bool } // Engine forwards domain operations to the single assembled use-case runner. type Engine struct { runner *usecase.Runner } // New assembles an engine from facade-selected configuration and collaborators. func New(cfg Config, options Options) (*Engine, error) { promptDefinitions := options.PromptDefinitions if !options.PromptSource { if strings.TrimSpace(cfg.PromptDir) == "" { return nil, fmt.Errorf("prompt directory is required") } promptDefinitions = promptdef.NewFilesystemRepository(cfg.PromptDir) } maintained, err := catalog.Load( catalog.Source{Name: "OpenRouter", ExpectedBackendID: backend.OpenRouterID, FS: openrouter.FS(), Root: openrouter.Root}, catalog.Source{Name: "Rakestrawhome", ExpectedBackendID: backend.RakestrawHomeID, FS: rakestrawhome.FS(), Root: rakestrawhome.Root}, ) if err != nil { return nil, fmt.Errorf("failed to load maintained catalogs: %w", err) } profiles := newProfileRepository(cfg.ProfileDir, options, maintained.Profiles) registry, err := backend.NewRegistry(maintained.Backends, options.Backends) if err != nil { return nil, fmt.Errorf("failed to construct backend registry: %w", err) } manager, err := capacity.NewManager(registry.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) } client := options.LLMClient if client == nil { client, err = llm.NewOpenAICompatibleClient(llm.OpenAICompatibleConfig{Timeout: cfg.Timeout, HTTPClient: cfg.HTTPClient}) if err != nil { return nil, err } } client = capacity.NewClient(manager, client) reader := options.ArtifactReader if !options.ArtifactSource { reader = artifact.NewCompositeReader() } return &Engine{runner: usecase.NewRunnerWithRepairer(promptDefinitions, profiles, registry, reader, prompt.NewGoRenderer(), client, validator, usecase.NewDefaultOutputRepairer(client), manager)}, nil } func newProfileRepository(profileDir string, options Options, maintained profile.Repository) profile.Repository { repository := maintained if options.FallbackProfileSource { repository = profile.NewOverlayRepository(options.FallbackProfiles, repository) } if options.ProfileSource { repository = profile.NewOverlayRepository(options.Profiles, repository) } else if strings.TrimSpace(profileDir) != "" { repository = profile.NewOverlayRepository(profile.NewFilesystemRepository(profileDir), repository) } if options.MemorySource { repository = profile.NewOverlayRepository(options.MemoryProfiles, repository) } return profile.NewResolvingRepository(repository) } func (e *Engine) InspectPrompt(ctx context.Context, id, version string) (*domain.PromptInspection, error) { return e.runner.InspectPrompt(ctx, id, version) } func (e *Engine) InspectProfile(ctx context.Context, id string) (*domain.ProfileInspection, error) { return e.runner.InspectProfile(ctx, id) } 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) }