From bd2b7b1862ff8ed164b38499c71726f6276668a1 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Tue, 22 Sep 2026 04:34:36 +0000 Subject: [PATCH] Move engine composition into the internal engine package --- docs/internal/overview.md | 3 +- docs/internal/runner.md | 8 +- docs/internal/sources.md | 4 +- docs/policy/architecture.md | 4 +- docs/roadmap/implementation.md | 9 ++ engine.go | 132 ++++++------------------------ internal/engine/engine.go | 145 +++++++++++++++++++++++++++++++++ 7 files changed, 192 insertions(+), 113 deletions(-) create mode 100644 internal/engine/engine.go diff --git a/docs/internal/overview.md b/docs/internal/overview.md index 0d4bbb9..6354074 100644 --- a/docs/internal/overview.md +++ b/docs/internal/overview.md @@ -11,13 +11,14 @@ contributor workflow and validation. | Component | Implemented responsibility | References | | --- | --- | --- | -| Root `promptkit` package | Provides the supported engine facade, source, backend-registration, and injection options, public request, result, prompt-inspection, and profile-inspection values, opaque prepared-execution handles, profile construction, extension interfaces, value conversion, redacted formatting, typed capacity and generation error mapping, engine-local profile-source assembly including application fallbacks, and bounded output-repair assembly. | [Package GoDoc](../../doc.go), [prepared execution](../../prepared_execution.go), [backend API](../../backends.go), [engine assembly](../../engine.go) | +| Root `promptkit` package | Provides the supported engine facade, source, backend-registration, and injection options, public request, result, prompt-inspection, and profile-inspection values, opaque prepared-execution handles, profile construction, extension interfaces, value conversion, redacted formatting, and typed capacity and generation error mapping. | [Package GoDoc](../../doc.go), [prepared execution](../../prepared_execution.go), [backend API](../../backends.go), [engine facade](../../engine.go) | | `examples/go-library/prepare` | Demonstrates an offline downstream consumer using a prompt file, in-memory profile, inline input, and `Prepare`. It is not a public library package. | [Example program](../../examples/go-library/prepare/main.go) | | `examples/go-library/run` | Demonstrates an offline downstream consumer using a prompt file, in-memory profile, inline input, an injected deterministic model client, and `Run`. It is not a public library package. | [Example program](../../examples/go-library/run/main.go) | | `internal/backend` | Constructs each engine's immutable registry from maintained definitions and consumer additions, validates and defensively copies definitions through the shared JSON-value package, and consumes the LLM-owned OpenAI-compatible reserved request-field rule. | [Backend registry](../../internal/backend/registry.go) | | `internal/catalog` | Strictly validates imported immutable maintained backend and profile catalog assets before engine assembly uses them. | [Catalog adapter](../../internal/catalog/catalog.go), [internal sources](sources.md#profiles-and-built-ins) | | `internal/capacity` | Owns engine-local bounded execution admission and FIFO model-generation permits for limited backend IDs, including cancellation-safe waiter removal and client wrapping. | [Internal capacity management](capacity.md) | | `internal/domain` | Defines internal framework values for requests, artifacts, prompt definitions, profiles, execution targets, rendering, generation, and validation, and owns source-neutral invariants for shared execution settings, OpenAI-compatible base endpoints, session identifiers, and output contracts. Source parsing, required fields, other source-specific normalization, defaulting, and boundary-specific error classification remain with their callers. | [Domain declarations](../../internal/domain/domain.go), [endpoint invariant](../../internal/domain/endpoint.go) | +| `internal/engine` | Assembles catalogs, repositories, registry, capacity, collaborators, repairer, and the one use-case runner for each engine instance. | [Engine assembly](../../internal/engine/engine.go), [internal runner](runner.md) | | `internal/defaults` | Defines application-neutral framework constants and constructs the default execution target. It contains no CLI, server, or inbound HTTP limits. | [Framework defaults](../../internal/defaults/defaults.go) | | `internal/filecatalog` | Provides deterministic YAML discovery and path helpers for operating-system filesystems and `fs.FS` sources. | [File catalog](../../internal/filecatalog/catalog.go) | | `internal/jsonvalue` | Validates and deeply copies bounded JSON-compatible extra-parameter and prepared-schema trees while preserving supported concrete value types and rejecting cycles or excessive depth and work. | [JSON values](../../internal/jsonvalue/jsonvalue.go) | diff --git a/docs/internal/runner.md b/docs/internal/runner.md index 46d51fe..5473739 100644 --- a/docs/internal/runner.md +++ b/docs/internal/runner.md @@ -9,8 +9,8 @@ artifact, rendering, and validation behavior, while the [model-client document](llm.md) owns generation behavior and failure categories. -The runner remains under `internal/usecase` and is assembled by the root -Promptkit engine. Its concrete type is not part of the public API. +The runner remains under `internal/usecase` and is assembled by +`internal/engine`. Its concrete type is not part of the public API. The [framework format reference](../formats.md) owns prompt, profile, schema, and override semantics consumed by the runner. @@ -18,12 +18,12 @@ and override semantics consumed by the runner. `Runner` coordinates narrow internal interfaces for prompt definitions, profiles, backend resolution, artifacts, rendering, model generation, and -validation. The root engine supplies one immutable registry containing the +validation. The internal engine supplies one immutable registry containing the built-in backend and validated consumer additions, one engine-local run admitter, and a model client wrapped by the same capacity manager. Validation plans and provider-facing schema metadata come from the validator's preparation interface. -The root engine supplies one default output repairer through the explicit +The internal engine supplies one default output repairer through the explicit runner constructor, using the same capacity-wrapped client as initial generation. The no-repair runner constructor remains available for focused internal callers and tests. diff --git a/docs/internal/sources.md b/docs/internal/sources.md index 8e4034e..e9c8d73 100644 --- a/docs/internal/sources.md +++ b/docs/internal/sources.md @@ -6,7 +6,7 @@ This document describes Promptkit's implemented internal source, artifact, rendering, and output-validation behavior. The [architecture policy](../policy/architecture.md) owns the library boundary and dependency rules. None of these internal packages is a supported consumer API, -and the root engine assembles them behind its public source options and values. +and the internal engine assembles them behind the public facade's source options and values. The [framework format reference](../formats.md) owns the exact file fields, validation modes, built-in catalog, and source precedence. @@ -80,7 +80,7 @@ backend registry membership because the available registry belongs to the assembled engine; the runner checks membership during preparation and exact profile inspection. -The root engine assembles one raw composite catalog in precedence order: +The internal engine assembles one raw composite catalog in precedence order: in-memory profiles, one ordinary configured source, an application fallback source, then the maintained external catalog. An explicit file or `fs.FS` profile source replaces `Config.ProfileDir` within the ordinary configured-source diff --git a/docs/policy/architecture.md b/docs/policy/architecture.md index 9e44327..df206ab 100644 --- a/docs/policy/architecture.md +++ b/docs/policy/architecture.md @@ -46,12 +46,14 @@ The implemented internal components consist of: implements outbound OpenAI-compatible chat requests; and - `internal/usecase`, which coordinates preparation and execution across the internal framework components. +- `internal/engine`, which assembles those components into one engine and + delegates execution to the use-case runner. The `examples/go-library/prepare` and `examples/go-library/run` packages are maintained downstream consumers of the root facade. They do not expose library packages or participate in internal assembly. -The root facade assembles one immutable backend registry, one capacity manager, +The internal engine assembles one immutable backend registry, one capacity manager, the internal repositories, renderer, validator, outbound client, and use-case runner while translating public values and errors at the library boundary. The registry contains validated maintained definitions plus engine-scoped consumer diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 9d2003b..76dd97d 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -185,6 +185,15 @@ Acceptance: all public operations route through one internal engine and the existing runner; maintained catalogs and component wiring are internally owned. Prepared-execution behavior and public error contracts remain unchanged. +### Completion + +- [x] Added `internal/engine` as the owner of engine assembly and runner + delegation. +- [x] Kept the public facade's option application, conversions, nil checks, + prepared-execution wrapper, and error translation at the root. +- [x] Updated architecture, component, source, and runner documentation for + the implemented ownership split. + ## Stage 4: Move Option State And Source Construction Inward Purpose: finish the ownership split without changing the public option API. diff --git a/engine.go b/engine.go index 5e2ab8c..61aa844 100644 --- a/engine.go +++ b/engine.go @@ -11,17 +11,11 @@ import ( "strings" "time" - openrouter "gitea.maximumdirect.net/eric/promptkit-backend-openrouter" - rakestrawhome "gitea.maximumdirect.net/eric/promptkit-backend-rakestrawhome" artifactadapter "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" + internalengine "gitea.maximumdirect.net/eric/promptkit/internal/engine" "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" @@ -91,7 +85,7 @@ var ( // model generation. Injected collaborators may still be invoked concurrently // across different backend pools or for unlimited backends. type Engine struct { - runner *usecase.Runner + internal *internalengine.Engine } // Config selects the directory-backed sources and built-in model-client @@ -388,94 +382,22 @@ func NewEngine(cfg Config, opts ...Option) (*Engine, error) { } } - promptDefs := options.promptDefs - if !options.promptSource { - if strings.TrimSpace(cfg.PromptDir) == "" { - return nil, fmt.Errorf("%w: prompt directory is required", ErrInvalidConfig) - } - promptDefs = promptdef.NewFilesystemRepository(cfg.PromptDir) - } - - maintainedCatalogs, 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}, + assembled, err := internalengine.New( + internalengine.Config{PromptDir: cfg.PromptDir, ProfileDir: cfg.ProfileDir, SchemaDir: cfg.SchemaDir, Timeout: cfg.Timeout, HTTPClient: cfg.HTTPClient}, + internalengine.Options{ + LLMClient: options.llmClient, ArtifactReader: options.artifactReader, + PromptDefinitions: options.promptDefs, Profiles: options.profiles, + FallbackProfiles: options.fallbackProfiles, MemoryProfiles: options.memoryProfiles, + Backends: options.backends, Validator: options.validator, + PromptSource: options.promptSource, ProfileSource: options.profileSource, + FallbackProfileSource: options.fallbackProfileSource, MemorySource: options.memorySource, + ValidatorSource: options.validatorSource, ArtifactSource: options.artifactSource, + }, ) if err != nil { - return nil, fmt.Errorf("%w: failed to load maintained catalogs: %v", ErrInvalidConfig, err) + return nil, fmt.Errorf("%w: %v", ErrInvalidConfig, err) } - - profiles := newProfileRepository(cfg.ProfileDir, options, maintainedCatalogs.Profiles) - - backendRegistry, err := backend.NewRegistry(maintainedCatalogs.Backends, options.backends) - if err != nil { - return nil, fmt.Errorf("%w: failed to construct backend registry: %v", ErrInvalidConfig, err) - } - - capacityManager, err := capacity.NewManager(backendRegistry.CapacityPolicies()) - if err != nil { - return nil, fmt.Errorf("%w: failed to construct backend capacity manager: %v", ErrInvalidConfig, err) - } - - validator := options.validator - if !options.validatorSource { - schemaDir := cfg.SchemaDir - if strings.TrimSpace(schemaDir) == "" { - schemaDir = defaults.SchemaDirDefault - } - validator = validate.NewStandardValidator(schemaDir) - } - - llmClient := options.llmClient - if llmClient == nil { - var err error - llmClient, err = llm.NewOpenAICompatibleClient(llm.OpenAICompatibleConfig{ - Timeout: cfg.Timeout, - HTTPClient: cfg.HTTPClient, - }) - if err != nil { - return nil, fmt.Errorf("%w: %v", ErrInvalidConfig, err) - } - } - llmClient = capacity.NewClient(capacityManager, llmClient) - - artifacts := options.artifactReader - if !options.artifactSource { - artifacts = artifactadapter.NewCompositeReader() - } - - return &Engine{ - runner: usecase.NewRunnerWithRepairer( - promptDefs, - profiles, - backendRegistry, - artifacts, - prompt.NewGoRenderer(), - llmClient, - validator, - usecase.NewDefaultOutputRepairer(llmClient), - capacityManager, - ), - }, nil -} - -func newProfileRepository(profileDir string, options engineOptions, 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) + return &Engine{internal: assembled}, nil } func fileSource(name string) (fs.FS, string, error) { @@ -533,11 +455,11 @@ func (e *Engine) InspectPrompt( promptID string, promptVersion string, ) (*PromptInspection, error) { - if e == nil || e.runner == nil { + if e == nil || e.internal == nil { return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig) } - inspection, err := e.runner.InspectPrompt(ctx, promptID, promptVersion) + inspection, err := e.internal.InspectPrompt(ctx, promptID, promptVersion) if err != nil { return nil, mapPublicError(err) } @@ -573,11 +495,11 @@ func (e *Engine) InspectPrompt( // profile loading matches ErrProfileLoad while preserving the context error. // InspectProfile returns no partial result on error. func (e *Engine) InspectProfile(ctx context.Context, profileID string) (*ProfileInspection, error) { - if e == nil || e.runner == nil { + if e == nil || e.internal == nil { return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig) } - inspection, err := e.runner.InspectProfile(ctx, profileID) + inspection, err := e.internal.InspectProfile(ctx, profileID) if err != nil { return nil, mapPublicError(err) } @@ -604,7 +526,7 @@ func (e *Engine) InspectProfile(ctx context.Context, profileID string) (*Profile // relationship to ctx.Err is promised. Prepare returns no partial result on // error. func (e *Engine) Prepare(ctx context.Context, req RunRequest) (*PreparedRun, error) { - if e == nil || e.runner == nil { + if e == nil || e.internal == nil { return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig) } @@ -613,7 +535,7 @@ func (e *Engine) Prepare(ctx context.Context, req RunRequest) (*PreparedRun, err return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err) } - prepared, err := e.runner.Prepare(ctx, domainReq) + prepared, err := e.internal.Prepare(ctx, domainReq) if err != nil { return nil, mapPublicError(err) } @@ -637,7 +559,7 @@ func (e *Engine) Prepare(ctx context.Context, req RunRequest) (*PreparedRun, err // returns no handle on error. A nil Engine returns an error matching // ErrInvalidConfig. func (e *Engine) PrepareExecution(ctx context.Context, req RunRequest) (*PreparedExecution, error) { - if e == nil || e.runner == nil { + if e == nil || e.internal == nil { return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig) } @@ -646,7 +568,7 @@ func (e *Engine) PrepareExecution(ctx context.Context, req RunRequest) (*Prepare return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err) } - prepared, err := e.runner.PrepareExecution(ctx, domainReq) + prepared, err := e.internal.PrepareExecution(ctx, domainReq) if err != nil { return nil, mapPublicError(err) } @@ -678,7 +600,7 @@ func (e *Engine) PrepareExecution(ctx context.Context, req RunRequest) (*Prepare // behavior. A nil Engine returns ErrInvalidConfig. Run returns no partial // result on error. func (e *Engine) Run(ctx context.Context, req RunRequest) (*RunResult, error) { - if e == nil || e.runner == nil { + if e == nil || e.internal == nil { return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig) } @@ -687,7 +609,7 @@ func (e *Engine) Run(ctx context.Context, req RunRequest) (*RunResult, error) { return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err) } - result, err := e.runner.Run(ctx, domainReq) + result, err := e.internal.Run(ctx, domainReq) if err != nil { return nil, mapPublicError(err) } @@ -719,7 +641,7 @@ func (e *Engine) Run(ctx context.Context, req RunRequest) (*RunResult, error) { // including repair exhaustion, is returned in RunResult, not as an operational // error. An operational error returns no partial RunResult. func (e *Engine) RunPrepared(ctx context.Context, prepared *PreparedExecution) (*RunResult, error) { - if e == nil || e.runner == nil { + if e == nil || e.internal == nil { return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig) } @@ -727,7 +649,7 @@ func (e *Engine) RunPrepared(ctx context.Context, prepared *PreparedExecution) ( if prepared != nil { internal = prepared.internal } - result, err := e.runner.RunPrepared(ctx, internal) + result, err := e.internal.RunPrepared(ctx, internal) if err != nil { return nil, mapPublicError(err) } diff --git a/internal/engine/engine.go b/internal/engine/engine.go new file mode 100644 index 0000000..b5ec7be --- /dev/null +++ b/internal/engine/engine.go @@ -0,0 +1,145 @@ +// 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) +}