2026-07-28 04:43:19 +00:00
|
|
|
|
package promptkit
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"errors"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"io/fs"
|
|
|
|
|
|
"net/http"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
2026-09-30 03:18:49 +00:00
|
|
|
|
"gitea.maximumdirect.net/eric/ConfigKit"
|
2026-07-29 17:15:51 +00:00
|
|
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
2026-09-22 04:34:36 +00:00
|
|
|
|
internalengine "gitea.maximumdirect.net/eric/promptkit/internal/engine"
|
2026-09-23 17:11:09 +00:00
|
|
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/observation"
|
2026-07-28 04:43:19 +00:00
|
|
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/usecase"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-07-29 14:06:54 +00:00
|
|
|
|
// ErrInvalidConfig identifies invalid engine construction, including missing
|
2026-09-30 03:18:49 +00:00
|
|
|
|
// effective configuration, invalid options, and a nil
|
2026-07-29 17:22:55 +00:00
|
|
|
|
// Engine receiver.
|
2026-07-28 04:43:19 +00:00
|
|
|
|
var ErrInvalidConfig = errors.New("invalid engine configuration")
|
|
|
|
|
|
|
|
|
|
|
|
var (
|
2026-07-29 14:06:54 +00:00
|
|
|
|
// ErrInvalidRequest identifies a request whose required values, overrides,
|
|
|
|
|
|
// credentials, or effective settings are invalid.
|
|
|
|
|
|
ErrInvalidRequest = errors.New("invalid run request")
|
2026-10-17 05:47:14 +00:00
|
|
|
|
// ErrPromptNotFound identifies a requested prompt ID that is not present in
|
|
|
|
|
|
// the captured configuration. It does not also match ErrPromptLoad.
|
2026-07-29 14:06:54 +00:00
|
|
|
|
ErrPromptNotFound = errors.New("prompt not found")
|
|
|
|
|
|
// ErrProfileNotFound identifies a selected profile ID that is absent from
|
|
|
|
|
|
// every configured profile source. It does not also match ErrProfileLoad.
|
|
|
|
|
|
ErrProfileNotFound = errors.New("profile not found")
|
|
|
|
|
|
// ErrProfileRequired identifies a request for which neither RunRequest.ProfileID
|
|
|
|
|
|
// nor the selected prompt's default profile is present. Such an error also
|
|
|
|
|
|
// matches ErrInvalidRequest.
|
|
|
|
|
|
ErrProfileRequired = errors.New("profile selection is required")
|
2026-09-30 03:18:49 +00:00
|
|
|
|
// ErrPromptLoad identifies a prompt selection failure other than the
|
|
|
|
|
|
// not-found case represented by ErrPromptNotFound. Invalid definitions
|
|
|
|
|
|
// instead fail construction with ErrInvalidConfig.
|
2026-07-29 14:06:54 +00:00
|
|
|
|
ErrPromptLoad = errors.New("failed to load prompt definition")
|
2026-09-30 03:18:49 +00:00
|
|
|
|
// ErrProfileLoad identifies a profile selection or resolution failure other
|
|
|
|
|
|
// than the not-found case represented by ErrProfileNotFound. Invalid
|
|
|
|
|
|
// definitions instead fail construction with ErrInvalidConfig.
|
2026-07-29 14:06:54 +00:00
|
|
|
|
ErrProfileLoad = errors.New("failed to load execution profile")
|
2026-08-25 00:44:26 +00:00
|
|
|
|
// ErrAPIKeyEnvMissing identifies an explicitly required APIKeyEnv whose
|
|
|
|
|
|
// environment variable is unset or empty after direct RunRequest.APIKey
|
|
|
|
|
|
// precedence is applied. Such an error also matches ErrInvalidRequest.
|
2026-07-28 04:43:19 +00:00
|
|
|
|
ErrAPIKeyEnvMissing = errors.New("api_key_env points to an unset environment variable")
|
2026-07-29 14:06:54 +00:00
|
|
|
|
// ErrArtifactLoad identifies a failure to resolve an input artifact. Errors
|
|
|
|
|
|
// returned by an injected ArtifactReader remain available through errors.Is.
|
|
|
|
|
|
ErrArtifactLoad = errors.New("failed to load artifact")
|
|
|
|
|
|
// ErrPromptRender identifies a failure to render prompt messages or the
|
|
|
|
|
|
// session ID from the resolved inputs and variables.
|
|
|
|
|
|
ErrPromptRender = errors.New("failed to render prompt")
|
2026-07-30 18:20:35 +00:00
|
|
|
|
// ErrCapacityExceeded identifies a Run or RunPrepared rejected because the
|
|
|
|
|
|
// selected backend already admitted ConcurrencyLimit + QueueCapacity calls.
|
2026-07-30 23:24:04 +00:00
|
|
|
|
// A [CapacityError] reports the selected backend ID. It is not an invalid
|
|
|
|
|
|
// request, an LLM or provider rate-limit response, or ErrLLMGenerate.
|
2026-07-29 21:11:08 +00:00
|
|
|
|
ErrCapacityExceeded = errors.New("backend capacity exceeded")
|
2026-07-29 14:06:54 +00:00
|
|
|
|
// ErrLLMGenerate identifies a model-client failure or a nil successful
|
2026-08-23 19:01:38 +00:00
|
|
|
|
// response. A built-in OpenAI-compatible non-2xx response is available as a
|
|
|
|
|
|
// [GenerationError]. Errors returned by an injected LLMClient remain
|
|
|
|
|
|
// available through errors.Is.
|
2026-07-29 14:06:54 +00:00
|
|
|
|
ErrLLMGenerate = errors.New("failed to generate output")
|
2026-09-23 16:37:47 +00:00
|
|
|
|
// ErrTransport identifies a failed HTTP request by the built-in model
|
2026-09-23 16:56:20 +00:00
|
|
|
|
// client. Generation errors in this category also match ErrLLMGenerate and
|
|
|
|
|
|
// retain the transport cause for errors.Is and errors.As. It does not imply
|
|
|
|
|
|
// that the provider received no work.
|
2026-09-23 16:37:47 +00:00
|
|
|
|
ErrTransport = errors.New("model transport failed")
|
|
|
|
|
|
// ErrMalformedResponse identifies an invalid successful model response or
|
2026-09-23 16:56:20 +00:00
|
|
|
|
// a nil successful response from an injected client. Generation errors in
|
|
|
|
|
|
// this category also match ErrLLMGenerate.
|
2026-09-23 16:37:47 +00:00
|
|
|
|
ErrMalformedResponse = errors.New("malformed model response")
|
2026-09-23 16:56:20 +00:00
|
|
|
|
// ErrRetriesExhausted identifies a built-in generation sequence whose
|
|
|
|
|
|
// positive transient retry budget ended with another eligible HTTP status.
|
|
|
|
|
|
// RetryExhaustedError retains the final cause and actual attempt count.
|
|
|
|
|
|
ErrRetriesExhausted = errors.New("transient model retries exhausted")
|
2026-07-29 14:06:54 +00:00
|
|
|
|
// ErrValidation identifies an operational failure to load or compile a
|
|
|
|
|
|
// schema or validate output. A completed validation whose Status is
|
|
|
|
|
|
// ValidationFailed is returned in RunResult without this error.
|
|
|
|
|
|
ErrValidation = errors.New("failed to validate output")
|
2026-07-28 04:43:19 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-10-08 20:40:16 +00:00
|
|
|
|
// Engine inspects prompts and profiles, enumerates prompt requirements, and
|
|
|
|
|
|
// prepares and runs Promptkit prompt requests.
|
2026-07-29 14:06:54 +00:00
|
|
|
|
//
|
2026-09-30 03:29:28 +00:00
|
|
|
|
// An Engine is safe for concurrent calls to [Engine.InspectConfiguration],
|
|
|
|
|
|
// [Engine.InspectPrompt],
|
2026-10-08 20:40:16 +00:00
|
|
|
|
// [Engine.InspectRequirements], [Engine.InspectProfile], [Engine.Prepare], [Engine.PrepareExecution],
|
2026-10-13 18:16:34 +00:00
|
|
|
|
// [Engine.Run], [Engine.RunPrepared], and [Engine.SmokeTest]. Each Engine owns
|
|
|
|
|
|
// independent backend-capacity pools that coordinate Run, RunPrepared, and
|
|
|
|
|
|
// inference SmokeTest admission and model generation. Injected collaborators
|
|
|
|
|
|
// may still be invoked concurrently across different backend pools or for
|
|
|
|
|
|
// unlimited backends.
|
2026-07-28 04:43:19 +00:00
|
|
|
|
type Engine struct {
|
2026-09-22 04:34:36 +00:00
|
|
|
|
internal *internalengine.Engine
|
2026-07-28 04:43:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-30 03:18:49 +00:00
|
|
|
|
// ConfigurationSources selects ordered ConfigKit underlays and an optional
|
|
|
|
|
|
// highest-priority main input. Each underlay's inputs strictly compose within
|
|
|
|
|
|
// their group; later underlays overlay earlier ones. A nil Main uses
|
|
|
|
|
|
// ConfigKit's empty source. A non-nil Main is passed through unchanged and
|
|
|
|
|
|
// may fail construction if invalid.
|
|
|
|
|
|
type ConfigurationSources struct {
|
|
|
|
|
|
Underlays []configkit.Underlay
|
|
|
|
|
|
Main *configkit.Input
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Config selects configuration sources and built-in model-client transport
|
|
|
|
|
|
// used by [NewEngine]. Config has no stable JSON representation.
|
2026-07-28 04:43:19 +00:00
|
|
|
|
type Config struct {
|
2026-09-30 03:18:49 +00:00
|
|
|
|
// Sources combines maintained definitions with caller-selected inputs.
|
|
|
|
|
|
Sources ConfigurationSources
|
2026-07-29 14:06:54 +00:00
|
|
|
|
// SchemaDir is the root for JSON Schema files. An empty value uses the
|
|
|
|
|
|
// current directory. WithSchemaFS or WithSchemaFile replaces this source.
|
|
|
|
|
|
SchemaDir string
|
2026-07-28 04:43:19 +00:00
|
|
|
|
// Timeout is the transport-wide safety cap for the built-in LLM client
|
2026-07-29 14:06:54 +00:00
|
|
|
|
// when HTTPClient is absent or has a non-positive timeout. A zero or negative
|
|
|
|
|
|
// value selects the 10-minute default.
|
2026-07-28 04:43:19 +00:00
|
|
|
|
Timeout time.Duration
|
|
|
|
|
|
// HTTPClient is cloned for the built-in LLM client. Its positive Timeout
|
2026-07-29 14:06:54 +00:00
|
|
|
|
// takes precedence over Timeout. A zero or negative client Timeout inherits
|
|
|
|
|
|
// Timeout or the 10-minute default. The supplied client is not mutated. This
|
|
|
|
|
|
// field is ignored when WithLLMClient is used.
|
2026-07-28 04:43:19 +00:00
|
|
|
|
HTTPClient *http.Client
|
2026-09-23 16:56:20 +00:00
|
|
|
|
// TransientRetries selects additional built-in HTTP attempts after eligible
|
|
|
|
|
|
// provider statuses. Nil defaults to two; zero disables retries. Negative
|
|
|
|
|
|
// values are invalid. Injected LLMClient implementations are never retried
|
|
|
|
|
|
// automatically.
|
|
|
|
|
|
TransientRetries *int
|
2026-07-28 04:43:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Option customizes engine construction.
|
2026-07-29 14:06:54 +00:00
|
|
|
|
//
|
|
|
|
|
|
// NewEngine applies options in argument order and ignores nil options. Within
|
2026-09-30 03:18:49 +00:00
|
|
|
|
// each schema-source, model-client, and artifact-reader category, the last
|
|
|
|
|
|
// non-nil valid option replaces earlier options. An invalid option fails
|
|
|
|
|
|
// construction even if a later option would replace it.
|
2026-07-28 04:43:19 +00:00
|
|
|
|
type Option interface {
|
|
|
|
|
|
apply(*engineOptions) error
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type optionFunc func(*engineOptions) error
|
|
|
|
|
|
|
|
|
|
|
|
func (f optionFunc) apply(options *engineOptions) error {
|
|
|
|
|
|
return f(options)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type engineOptions struct {
|
2026-09-22 04:44:41 +00:00
|
|
|
|
internal internalengine.Options
|
2026-07-28 04:43:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-30 19:06:17 +00:00
|
|
|
|
// WithLLMClient replaces the built-in model client used by [Engine.Run] and
|
|
|
|
|
|
// [Engine.RunPrepared].
|
2026-07-29 14:06:54 +00:00
|
|
|
|
//
|
2026-07-29 21:11:08 +00:00
|
|
|
|
// A nil client makes NewEngine fail with ErrInvalidConfig. The Engine schedules
|
|
|
|
|
|
// Generate calls according to the selected backend's capacity policy, but the
|
|
|
|
|
|
// client may still be called concurrently across different backend pools or for
|
2026-07-30 19:06:17 +00:00
|
|
|
|
// unlimited backends. The client is not used by [Engine.Prepare] or
|
|
|
|
|
|
// [Engine.PrepareExecution].
|
2026-07-28 04:43:19 +00:00
|
|
|
|
func WithLLMClient(client LLMClient) Option {
|
|
|
|
|
|
return optionFunc(func(options *engineOptions) error {
|
|
|
|
|
|
if client == nil {
|
|
|
|
|
|
return ErrInvalidConfig
|
|
|
|
|
|
}
|
2026-09-22 04:44:41 +00:00
|
|
|
|
options.internal.SetLLMClient(publicLLMClientAdapter{client: client})
|
2026-07-28 04:43:19 +00:00
|
|
|
|
return nil
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-29 14:06:54 +00:00
|
|
|
|
// WithArtifactReader replaces the default reader for every input artifact
|
|
|
|
|
|
// reference, regardless of its ArtifactRef.Type.
|
|
|
|
|
|
//
|
|
|
|
|
|
// A nil reader makes NewEngine fail with ErrInvalidConfig. The reader may be
|
|
|
|
|
|
// called concurrently.
|
2026-07-28 04:43:19 +00:00
|
|
|
|
func WithArtifactReader(reader ArtifactReader) Option {
|
|
|
|
|
|
return optionFunc(func(options *engineOptions) error {
|
|
|
|
|
|
if reader == nil {
|
|
|
|
|
|
return ErrInvalidConfig
|
|
|
|
|
|
}
|
2026-09-22 04:44:41 +00:00
|
|
|
|
options.internal.SetArtifactReader(publicArtifactReaderAdapter{reader: reader})
|
2026-07-28 04:43:19 +00:00
|
|
|
|
return nil
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// WithSchemaFS loads JSON Schema documents from fsys under root.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Prompt schema_path values resolve within this source when schema validation
|
2026-07-29 14:06:54 +00:00
|
|
|
|
// or structured output is requested. fsys must be non-nil and root must be
|
|
|
|
|
|
// non-empty; otherwise NewEngine fails with ErrInvalidConfig. This option
|
|
|
|
|
|
// replaces Config.SchemaDir and earlier schema-source options.
|
2026-07-28 04:43:19 +00:00
|
|
|
|
func WithSchemaFS(fsys fs.FS, root string) Option {
|
|
|
|
|
|
return optionFunc(func(options *engineOptions) error {
|
2026-09-22 04:44:41 +00:00
|
|
|
|
return options.internal.SetSchemaFS(fsys, root)
|
2026-07-28 04:43:19 +00:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// WithSchemaFile loads JSON Schema documents from the single schema file at path.
|
|
|
|
|
|
//
|
2026-07-29 14:06:54 +00:00
|
|
|
|
// Prompt schema_path values refer to the file's base name. path must name an
|
|
|
|
|
|
// existing non-directory file when NewEngine applies the option. This option
|
|
|
|
|
|
// replaces Config.SchemaDir and earlier schema-source options.
|
2026-07-28 04:43:19 +00:00
|
|
|
|
func WithSchemaFile(path string) Option {
|
|
|
|
|
|
return optionFunc(func(options *engineOptions) error {
|
2026-09-22 04:44:41 +00:00
|
|
|
|
return options.internal.SetSchemaFile(path)
|
2026-07-28 04:43:19 +00:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewEngine constructs an Engine from configuration and options.
|
2026-07-29 14:06:54 +00:00
|
|
|
|
//
|
2026-10-17 06:08:35 +00:00
|
|
|
|
// ConfigKit composes caller-selected underlays in their given order, followed
|
|
|
|
|
|
// by the optional main input. Construction validates the final effective
|
|
|
|
|
|
// backend, profile, and prompt definitions and captures referenced prompt
|
|
|
|
|
|
// content. A missing prompt collection is allowed. Schema documents and
|
|
|
|
|
|
// request artifacts remain runtime resources.
|
2026-07-29 14:06:54 +00:00
|
|
|
|
//
|
|
|
|
|
|
// NewEngine returns an error matching ErrInvalidConfig for invalid
|
2026-09-30 03:18:49 +00:00
|
|
|
|
// configuration or options, preserving a ConfigKit LoadError when loading
|
|
|
|
|
|
// fails. Each constructed
|
2026-07-29 21:11:08 +00:00
|
|
|
|
// Engine has independent backend-capacity pools. Construction does not perform
|
2026-09-23 16:56:20 +00:00
|
|
|
|
// model requests or require credentials. A negative Config.TransientRetries
|
|
|
|
|
|
// is invalid.
|
2026-07-28 04:43:19 +00:00
|
|
|
|
func NewEngine(cfg Config, opts ...Option) (*Engine, error) {
|
|
|
|
|
|
var options engineOptions
|
|
|
|
|
|
for _, opt := range opts {
|
|
|
|
|
|
if opt == nil {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := opt.apply(&options); err != nil {
|
2026-09-30 03:18:49 +00:00
|
|
|
|
return nil, fmt.Errorf("%w: %w", ErrInvalidConfig, err)
|
2026-07-28 04:43:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 04:34:36 +00:00
|
|
|
|
assembled, err := internalengine.New(
|
2026-09-30 03:18:49 +00:00
|
|
|
|
internalengine.Config{Sources: internalengine.ConfigurationSources{Underlays: cfg.Sources.Underlays, Main: cfg.Sources.Main}, SchemaDir: cfg.SchemaDir, Timeout: cfg.Timeout, HTTPClient: cfg.HTTPClient, TransientRetries: copyIntPtr(cfg.TransientRetries)},
|
2026-09-22 04:44:41 +00:00
|
|
|
|
options.internal,
|
2026-08-26 15:04:50 +00:00
|
|
|
|
)
|
|
|
|
|
|
if err != nil {
|
2026-09-30 03:18:49 +00:00
|
|
|
|
return nil, fmt.Errorf("%w: %w", ErrInvalidConfig, err)
|
2026-07-28 04:43:19 +00:00
|
|
|
|
}
|
2026-09-22 04:34:36 +00:00
|
|
|
|
return &Engine{internal: assembled}, nil
|
2026-08-01 12:31:50 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-17 05:47:14 +00:00
|
|
|
|
// InspectPrompt resolves one prompt ID without selecting a profile or starting
|
|
|
|
|
|
// execution work.
|
2026-07-30 21:03:47 +00:00
|
|
|
|
//
|
2026-10-17 05:47:14 +00:00
|
|
|
|
// InspectPrompt requires a nonblank promptID and performs an exact,
|
|
|
|
|
|
// case-sensitive lookup in the captured configuration snapshot.
|
2026-07-30 21:03:47 +00:00
|
|
|
|
//
|
|
|
|
|
|
// A successful result proves that the selected definition and any referenced
|
|
|
|
|
|
// message content files were structurally loaded. Inputs are returned in
|
|
|
|
|
|
// definition order. DefaultProfileID is declared metadata only and is not
|
|
|
|
|
|
// resolved. OutputContract is the normalized declared contract, with a JSON
|
|
|
|
|
|
// Schema path when declared but without loading or compiling that schema.
|
|
|
|
|
|
// PromptHash is the same opaque equality value as PreparedRun.PromptHash for
|
2026-09-30 03:18:49 +00:00
|
|
|
|
// the selected captured definition; its spelling, length,
|
2026-07-30 21:03:47 +00:00
|
|
|
|
// encoding, algorithm, and security properties are not contracts.
|
|
|
|
|
|
//
|
|
|
|
|
|
// This method does not return prompt bodies, templates, source paths, schemas,
|
|
|
|
|
|
// rendered messages, or execution settings. It does not resolve a profile or
|
|
|
|
|
|
// credential, read artifacts or schemas, render, validate, admit capacity,
|
|
|
|
|
|
// contact a provider, or generate model output. The returned PromptInspection
|
2026-09-30 03:18:49 +00:00
|
|
|
|
// and its input slice are caller-owned. Inspection does not reload configuration
|
|
|
|
|
|
// or content files; construct another Engine to observe source changes.
|
2026-07-30 21:03:47 +00:00
|
|
|
|
//
|
|
|
|
|
|
// A nil Engine returns an error matching ErrInvalidConfig. A blank prompt ID
|
2026-10-17 05:47:14 +00:00
|
|
|
|
// matches ErrInvalidRequest. An absent exact ID matches ErrPromptNotFound and
|
|
|
|
|
|
// not ErrPromptLoad. Invalid definitions and resource failures reject
|
|
|
|
|
|
// construction instead. Cancellation during lookup preserves the context
|
|
|
|
|
|
// error. InspectPrompt returns no partial result on error.
|
2026-07-30 21:03:47 +00:00
|
|
|
|
func (e *Engine) InspectPrompt(
|
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
|
promptID string,
|
|
|
|
|
|
) (*PromptInspection, error) {
|
2026-09-22 04:34:36 +00:00
|
|
|
|
if e == nil || e.internal == nil {
|
2026-07-30 21:03:47 +00:00
|
|
|
|
return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-17 05:47:14 +00:00
|
|
|
|
inspection, err := e.internal.InspectPrompt(ctx, promptID)
|
2026-07-30 21:03:47 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, mapPublicError(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return fromDomainPromptInspection(inspection), nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-30 19:52:00 +00:00
|
|
|
|
// InspectProfile resolves one explicit profile without selecting a prompt or
|
|
|
|
|
|
// starting execution work.
|
|
|
|
|
|
//
|
2026-09-30 03:18:49 +00:00
|
|
|
|
// InspectProfile looks up a nonblank profileID exactly and case-sensitively
|
|
|
|
|
|
// in the captured effective configuration. It applies the framework timeout
|
|
|
|
|
|
// baseline, selected
|
2026-08-01 12:37:01 +00:00
|
|
|
|
// backend, and then selected profile to EffectiveModelParams without a request
|
|
|
|
|
|
// override. BackendID is empty for an endpoint-only profile.
|
2026-07-30 19:52:00 +00:00
|
|
|
|
//
|
|
|
|
|
|
// APIKeyEnv in the returned target is an environment-variable name, never its
|
|
|
|
|
|
// value. APIKeyRequired instead reports a direct credential requirement and is
|
|
|
|
|
|
// mutually exclusive with a nonblank APIKeyEnv. InspectProfile neither derives
|
|
|
|
|
|
// an ID from a prompt default_profile nor checks credential availability, so an
|
|
|
|
|
|
// absent or blank named environment variable is not an error.
|
|
|
|
|
|
//
|
|
|
|
|
|
// The returned ProfileInspection and all nested mutable values are
|
2026-09-30 03:18:49 +00:00
|
|
|
|
// caller-owned. Inspection does not reload configuration; construct another
|
|
|
|
|
|
// Engine to observe source changes. This method does not load
|
2026-07-30 19:52:00 +00:00
|
|
|
|
// a prompt, render, read artifacts or schemas, admit backend capacity, contact
|
|
|
|
|
|
// a provider, or generate model output.
|
|
|
|
|
|
//
|
|
|
|
|
|
// A nil Engine returns an error matching ErrInvalidConfig. A blank profile ID
|
|
|
|
|
|
// matches ErrInvalidRequest. An absent exact ID matches ErrProfileNotFound and
|
2026-09-30 03:18:49 +00:00
|
|
|
|
// not ErrProfileLoad. Invalid definitions or unknown backends reject
|
|
|
|
|
|
// construction instead. Cancellation preserves the context error.
|
2026-07-30 19:52:00 +00:00
|
|
|
|
// InspectProfile returns no partial result on error.
|
|
|
|
|
|
func (e *Engine) InspectProfile(ctx context.Context, profileID string) (*ProfileInspection, error) {
|
2026-09-22 04:34:36 +00:00
|
|
|
|
if e == nil || e.internal == nil {
|
2026-07-30 19:52:00 +00:00
|
|
|
|
return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 04:34:36 +00:00
|
|
|
|
inspection, err := e.internal.InspectProfile(ctx, profileID)
|
2026-07-30 19:52:00 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, mapPublicError(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return fromDomainProfileInspection(inspection), nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-29 14:06:54 +00:00
|
|
|
|
// Prepare resolves and renders a prompt request without calling an LLM.
|
2026-08-26 02:19:44 +00:00
|
|
|
|
// It appends any validated RunRequest.AppendedMessages after rendered
|
|
|
|
|
|
// definition messages in the returned caller-owned snapshot.
|
2026-07-29 14:06:54 +00:00
|
|
|
|
//
|
2026-07-29 17:22:55 +00:00
|
|
|
|
// Prepare selects the prompt and profile, resolves any selected backend and
|
|
|
|
|
|
// effective execution settings, resolves the output contract, loads and hashes
|
|
|
|
|
|
// inputs, loads structured-output schema metadata when required, and renders
|
|
|
|
|
|
// the session ID and messages. The returned PreparedRun is owned by the caller
|
|
|
|
|
|
// and never contains a resolved API-key value, model output, or validation
|
|
|
|
|
|
// result.
|
2026-07-29 14:06:54 +00:00
|
|
|
|
//
|
|
|
|
|
|
// A nil Engine returns an error matching ErrInvalidConfig. Request and
|
|
|
|
|
|
// preparation failures may match ErrInvalidRequest, ErrPromptNotFound,
|
|
|
|
|
|
// ErrPromptLoad, ErrProfileNotFound, ErrProfileLoad, ErrProfileRequired,
|
|
|
|
|
|
// ErrAPIKeyEnvMissing, ErrArtifactLoad, ErrPromptRender, or ErrValidation as
|
|
|
|
|
|
// applicable. Cancellation is passed to the active collaborator and is
|
|
|
|
|
|
// reported in the applicable operation category; no general errors.Is
|
|
|
|
|
|
// relationship to ctx.Err is promised. Prepare returns no partial result on
|
2026-09-23 16:56:20 +00:00
|
|
|
|
// error. It validates RunRequest.TransientRetries but makes no model attempts.
|
2026-07-28 04:43:19 +00:00
|
|
|
|
func (e *Engine) Prepare(ctx context.Context, req RunRequest) (*PreparedRun, error) {
|
2026-10-12 17:43:11 +00:00
|
|
|
|
prepared, err := e.prepareRequest(ctx, req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return fromDomainPreparedRun(prepared), nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ValidateRequest reports whether req passes the same preparation path used by
|
|
|
|
|
|
// [Engine.Prepare] at the time of this call. A nil error means this concrete
|
2026-10-12 17:47:04 +00:00
|
|
|
|
// request passed preparation. It checks only this invocation and returns errors
|
|
|
|
|
|
// as encountered rather than aggregating them. All supplied inputs follow the
|
|
|
|
|
|
// same loading behavior as [Engine.Prepare], including inputs unused by the
|
|
|
|
|
|
// selected template branch. The method does not call an LLM or retain a
|
|
|
|
|
|
// prepared snapshot or executable handle, but preparation may still perform
|
|
|
|
|
|
// resource I/O and incur its associated time and memory costs.
|
2026-10-12 17:43:11 +00:00
|
|
|
|
//
|
|
|
|
|
|
// Its errors and context handling follow [Engine.Prepare], including an error
|
|
|
|
|
|
// matching ErrInvalidConfig when the Engine is nil. Success does not guarantee
|
2026-10-12 17:47:04 +00:00
|
|
|
|
// provider acceptance or later execution success. Use [Engine.PrepareExecution]
|
2026-10-12 17:43:11 +00:00
|
|
|
|
// when execution must use the snapshot produced by preparation.
|
|
|
|
|
|
func (e *Engine) ValidateRequest(ctx context.Context, req RunRequest) error {
|
|
|
|
|
|
_, err := e.prepareRequest(ctx, req)
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (e *Engine) prepareRequest(ctx context.Context, req RunRequest) (*domain.PreparedRun, error) {
|
2026-09-23 17:56:15 +00:00
|
|
|
|
ctx = observation.WithoutExecutionState(ctx)
|
2026-09-22 04:34:36 +00:00
|
|
|
|
if e == nil || e.internal == nil {
|
2026-07-28 04:43:19 +00:00
|
|
|
|
return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
domainReq, err := toDomainRunRequest(req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 04:34:36 +00:00
|
|
|
|
prepared, err := e.internal.Prepare(ctx, domainReq)
|
2026-07-28 04:43:19 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, mapPublicError(err)
|
|
|
|
|
|
}
|
2026-10-12 17:43:11 +00:00
|
|
|
|
return prepared, nil
|
2026-07-28 04:43:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-30 18:20:35 +00:00
|
|
|
|
// PrepareExecution completely prepares a prompt request without calling the
|
2026-08-26 02:19:44 +00:00
|
|
|
|
// configured LLMClient or reserving backend admission capacity. Validated
|
|
|
|
|
|
// RunRequest.AppendedMessages are included in the frozen effective messages.
|
2026-07-30 18:20:35 +00:00
|
|
|
|
//
|
|
|
|
|
|
// The returned opaque handle is bound to this Engine and permits one
|
|
|
|
|
|
// [Engine.RunPrepared] invocation. Preparation freezes the selected sources,
|
|
|
|
|
|
// rendered messages, effective settings, inputs, provider structured-output
|
|
|
|
|
|
// metadata, and validation resources needed by that invocation. The handle
|
|
|
|
|
|
// retains a direct RunRequest.APIKey only in private execution state;
|
|
|
|
|
|
// [PreparedExecution.Details] is credential-redacted.
|
|
|
|
|
|
//
|
|
|
|
|
|
// The context governs preparation only. Cancellation after this method
|
|
|
|
|
|
// returns does not invalidate the handle or propagate to RunPrepared.
|
|
|
|
|
|
// PrepareExecution returns the same error categories as [Engine.Prepare] and
|
|
|
|
|
|
// returns no handle on error. A nil Engine returns an error matching
|
2026-09-23 16:56:20 +00:00
|
|
|
|
// ErrInvalidConfig. The effective transient retry budget is frozen privately
|
2026-09-23 17:15:51 +00:00
|
|
|
|
// for RunPrepared and is absent from Details. A supplied Observer is retained
|
|
|
|
|
|
// privately for RunPrepared; preparation itself emits no events.
|
2026-07-30 18:20:35 +00:00
|
|
|
|
func (e *Engine) PrepareExecution(ctx context.Context, req RunRequest) (*PreparedExecution, error) {
|
2026-09-23 17:56:15 +00:00
|
|
|
|
ctx = observation.WithoutExecutionState(ctx)
|
2026-09-22 04:34:36 +00:00
|
|
|
|
if e == nil || e.internal == nil {
|
2026-07-30 18:20:35 +00:00
|
|
|
|
return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
domainReq, err := toDomainRunRequest(req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 04:34:36 +00:00
|
|
|
|
prepared, err := e.internal.PrepareExecution(ctx, domainReq)
|
2026-07-30 18:20:35 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, mapPublicError(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return &PreparedExecution{internal: prepared}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-29 14:06:54 +00:00
|
|
|
|
// Run prepares a request, invokes the configured LLMClient, and validates the
|
2026-09-30 03:18:49 +00:00
|
|
|
|
// generated output. Each call uses the Engine's captured configuration and
|
|
|
|
|
|
// composes a fresh effective prompt with any validated
|
|
|
|
|
|
// RunRequest.AppendedMessages.
|
2026-07-29 14:06:54 +00:00
|
|
|
|
//
|
|
|
|
|
|
// A content-validation failure is a successful run whose
|
2026-08-25 09:55:19 +00:00
|
|
|
|
// RunResult.Validation has Status ValidationFailed. When its output contract
|
|
|
|
|
|
// has a positive repair budget, a failed eligible validation can make bounded
|
|
|
|
|
|
// additional model calls and stops at the first valid candidate. Exhaustion
|
|
|
|
|
|
// returns the final failed validation result with cumulative usage and actual
|
|
|
|
|
|
// repair attempts. An inability to generate or validate returns an error and
|
|
|
|
|
|
// no partial result.
|
2026-07-29 14:06:54 +00:00
|
|
|
|
//
|
2026-09-23 16:56:20 +00:00
|
|
|
|
// The built-in client retries eligible provider statuses using the effective
|
|
|
|
|
|
// Config or request budget. With R additional transient attempts and P repair
|
|
|
|
|
|
// generations, one run can dispatch at most (R+1)×(P+1) Promptkit-controlled
|
|
|
|
|
|
// model attempts. Each dispatched attempt receives its own generation and
|
|
|
|
|
|
// HTTP-client timeout; the caller context governs the full operation.
|
|
|
|
|
|
// Injected LLMClient implementations are not retried automatically.
|
2026-09-23 17:29:12 +00:00
|
|
|
|
// With Observer, Run emits started before facade request conversion or a nil
|
|
|
|
|
|
// engine check, and finished on every normal return after public error mapping
|
|
|
|
|
|
// and resource release. The event RunID matches a successful result. Without
|
|
|
|
|
|
// Observer, result timing retains its existing convention; observed wall
|
|
|
|
|
|
// duration can be slightly longer because it spans the facade boundary.
|
2026-09-23 16:56:20 +00:00
|
|
|
|
//
|
2026-07-29 14:06:54 +00:00
|
|
|
|
// Run can return every error category documented by [Engine.Prepare], plus
|
2026-09-23 16:37:47 +00:00
|
|
|
|
// ErrCapacityExceeded and ErrLLMGenerate. Built-in HTTP execution failures
|
|
|
|
|
|
// additionally match ErrTransport; malformed successful responses and nil
|
|
|
|
|
|
// successful injected responses additionally match ErrMalformedResponse.
|
|
|
|
|
|
// An engine admission rejection is
|
2026-07-30 23:24:04 +00:00
|
|
|
|
// discoverable as [CapacityError] and still matches ErrCapacityExceeded. It
|
|
|
|
|
|
// occurs before artifacts, schemas, rendering, or model generation because the
|
|
|
|
|
|
// selected backend's admission capacity is full; it does not match
|
2026-08-23 19:01:38 +00:00
|
|
|
|
// ErrInvalidRequest or ErrLLMGenerate. A built-in OpenAI-compatible non-2xx
|
2026-09-23 16:56:20 +00:00
|
|
|
|
// response is discoverable as [GenerationError]. Eligible-status exhaustion
|
|
|
|
|
|
// with a positive budget is discoverable as [RetryExhaustedError]. Injected
|
|
|
|
|
|
// errors remain available through errors.Is. Cancellation while waiting for
|
2026-08-23 19:01:38 +00:00
|
|
|
|
// model-generation capacity matches both ErrLLMGenerate and the context error.
|
|
|
|
|
|
// Cancellation otherwise follows the active collaborator's documented
|
|
|
|
|
|
// behavior. A nil Engine returns ErrInvalidConfig. Run returns no partial
|
|
|
|
|
|
// result on error.
|
2026-07-28 04:43:19 +00:00
|
|
|
|
func (e *Engine) Run(ctx context.Context, req RunRequest) (*RunResult, error) {
|
2026-09-23 17:56:15 +00:00
|
|
|
|
ctx = observation.WithoutExecutionState(ctx)
|
2026-09-23 17:11:09 +00:00
|
|
|
|
if req.Observer == nil {
|
|
|
|
|
|
return e.run(ctx, req)
|
|
|
|
|
|
}
|
|
|
|
|
|
recorder, idErr := observation.New(func(event domain.ExecutionEvent) { req.Observer(fromDomainExecutionEvent(event)) })
|
|
|
|
|
|
recorder.Start()
|
|
|
|
|
|
if idErr != nil {
|
|
|
|
|
|
err := fmt.Errorf("failed to create run id: %w", idErr)
|
|
|
|
|
|
terminal := observedFinish(nil, err)
|
|
|
|
|
|
recorder.Finish(observation.FinishData{Outcome: terminal.Outcome, Failure: terminal.Failure})
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
result, err := e.run(observation.WithRecorder(ctx, recorder), req)
|
|
|
|
|
|
terminal := observedFinish(result, err)
|
|
|
|
|
|
recorder.Finish(observation.FinishData{Outcome: terminal.Outcome, Failure: terminal.Failure,
|
|
|
|
|
|
StatusCode: terminal.StatusCode, RetriesExhausted: terminal.RetriesExhausted})
|
|
|
|
|
|
return result, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (e *Engine) run(ctx context.Context, req RunRequest) (*RunResult, error) {
|
2026-09-22 04:34:36 +00:00
|
|
|
|
if e == nil || e.internal == nil {
|
2026-07-28 04:43:19 +00:00
|
|
|
|
return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
domainReq, err := toDomainRunRequest(req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 04:34:36 +00:00
|
|
|
|
result, err := e.internal.Run(ctx, domainReq)
|
2026-07-28 04:43:19 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, mapPublicError(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return fromDomainRunResult(result), nil
|
|
|
|
|
|
}
|
2026-07-30 18:20:35 +00:00
|
|
|
|
|
|
|
|
|
|
// RunPrepared atomically claims and executes a handle created by
|
|
|
|
|
|
// [Engine.PrepareExecution].
|
|
|
|
|
|
//
|
|
|
|
|
|
// A valid owning-Engine invocation consumes the handle's one attempt before
|
|
|
|
|
|
// credential revalidation, backend admission, generation, or validation.
|
|
|
|
|
|
// Cancellation, capacity rejection, generation failure, operational
|
|
|
|
|
|
// validation failure, and success all leave the handle unusable. A nil,
|
|
|
|
|
|
// zero-value, foreign-Engine, discarded, claimed, or used handle returns an
|
|
|
|
|
|
// error matching ErrInvalidRequest; a nil Engine returns ErrInvalidConfig and
|
|
|
|
|
|
// does not claim the handle.
|
|
|
|
|
|
//
|
|
|
|
|
|
// The supplied context governs this execution attempt independently of the
|
|
|
|
|
|
// preparation context. It covers credential revalidation, admission,
|
2026-08-25 09:55:19 +00:00
|
|
|
|
// generation, validation, and any bounded output repair. Result timing begins
|
|
|
|
|
|
// after the claim and excludes preparation and consumer-held delay.
|
2026-07-30 18:20:35 +00:00
|
|
|
|
//
|
|
|
|
|
|
// RunPrepared can return ErrInvalidRequest, ErrAPIKeyEnvMissing,
|
|
|
|
|
|
// ErrCapacityExceeded, ErrLLMGenerate, or ErrValidation as applicable while
|
2026-07-30 23:24:04 +00:00
|
|
|
|
// preserving documented collaborator and context identities. An engine
|
|
|
|
|
|
// admission rejection is discoverable as [CapacityError] and still matches
|
2026-08-23 19:01:38 +00:00
|
|
|
|
// ErrCapacityExceeded. A built-in OpenAI-compatible non-2xx response is
|
2026-09-23 16:37:47 +00:00
|
|
|
|
// discoverable as [GenerationError]. Built-in HTTP execution failures match
|
|
|
|
|
|
// ErrTransport; malformed successful responses and nil successful injected
|
2026-09-23 16:56:20 +00:00
|
|
|
|
// responses match ErrMalformedResponse. Positive-budget eligible-status
|
|
|
|
|
|
// exhaustion is discoverable as [RetryExhaustedError]. A completed
|
|
|
|
|
|
// content-validation rejection, including repair exhaustion, is returned in
|
|
|
|
|
|
// RunResult, not as an operational
|
|
|
|
|
|
// error. RunPrepared uses the retry choice frozen during PrepareExecution. An
|
2026-09-23 17:15:51 +00:00
|
|
|
|
// operational error returns no partial RunResult. A retained Observer begins
|
|
|
|
|
|
// only after a successful atomic claim, with identities from the frozen
|
|
|
|
|
|
// snapshot, and receives a terminal event on normal return after public error
|
|
|
|
|
|
// mapping. Invalid, foreign, discarded, or reused handles emit no events.
|
|
|
|
|
|
// Callbacks run outside the handle and capacity locks; panics propagate and
|
|
|
|
|
|
// do not promise a terminal event.
|
2026-07-30 18:20:35 +00:00
|
|
|
|
func (e *Engine) RunPrepared(ctx context.Context, prepared *PreparedExecution) (*RunResult, error) {
|
2026-09-23 17:56:15 +00:00
|
|
|
|
ctx = observation.WithoutExecutionState(ctx)
|
2026-09-22 04:34:36 +00:00
|
|
|
|
if e == nil || e.internal == nil {
|
2026-07-30 18:20:35 +00:00
|
|
|
|
return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var internal *usecase.PreparedExecution
|
|
|
|
|
|
if prepared != nil {
|
|
|
|
|
|
internal = prepared.internal
|
|
|
|
|
|
}
|
2026-09-23 17:15:51 +00:00
|
|
|
|
var recorder *observation.Recorder
|
|
|
|
|
|
result, err := e.internal.RunPrepared(observation.WithClaimSink(ctx, &recorder), internal)
|
2026-07-30 18:20:35 +00:00
|
|
|
|
if err != nil {
|
2026-09-23 17:15:51 +00:00
|
|
|
|
mapped := mapPublicError(err)
|
|
|
|
|
|
if recorder != nil {
|
|
|
|
|
|
terminal := observedFinish(nil, mapped)
|
|
|
|
|
|
recorder.Finish(observation.FinishData{Outcome: terminal.Outcome, Failure: terminal.Failure,
|
|
|
|
|
|
StatusCode: terminal.StatusCode, RetriesExhausted: terminal.RetriesExhausted})
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil, mapped
|
2026-07-30 18:20:35 +00:00
|
|
|
|
}
|
2026-09-23 17:15:51 +00:00
|
|
|
|
publicResult := fromDomainRunResult(result)
|
|
|
|
|
|
if recorder != nil {
|
|
|
|
|
|
terminal := observedFinish(publicResult, nil)
|
|
|
|
|
|
recorder.Finish(observation.FinishData{Outcome: terminal.Outcome})
|
|
|
|
|
|
}
|
|
|
|
|
|
return publicResult, nil
|
2026-07-30 18:20:35 +00:00
|
|
|
|
}
|