11 KiB
Profile Inheritance Roadmap
Purpose
Allow a Promptkit execution profile to derive from another profile. This gives downstream consumers stable, application-owned profile IDs without requiring them to copy a built-in or shared profile's model and execution settings.
For example, Weatherreporter should be able to select weather-light from its
prompt definitions while defining that profile as an alias or refinement of a
Promptkit built-in:
id: weather-light
base_profile: deepseek-4-flash
reasoning_effort: high
timeout_seconds: 120
Changing only base_profile to another profile later should redirect every
prompt that selects weather-light, without requiring changes to deployed
prompt definitions. The inherited profile remains ordinary Promptkit
configuration rather than application-specific routing logic.
Target End State
- File and
fs.FSprofile definitions may use the optionalbase_profilefield to name one parent profile. - In-memory profiles expose the equivalent
Profile.BaseProfileIDfield.OpenAICompatibleProfileConfigexposes and forwards the same field so its convenience constructor remains feature-complete. - A profile containing only
idandbase_profileis a valid semantic alias. Fields needed for an executable target may be inherited instead of repeated. - Base profiles may come from any configured profile source, including the embedded built-in catalog. Resolution uses the engine's complete assembled profile catalog and its existing source precedence.
- A child may refine selected inherited settings using the ordinary profile fields. After inheritance resolves, backend defaults and request overrides retain their existing precedence.
Prepare,PrepareExecution,Run, andInspectProfileall use the same inheritance behavior. Prepared execution freezes the fully resolved target and does not reopen the profile chain when it later runs.- Public results continue to report the selected child ID, such as
weather-light, while effective backend, endpoint, model, credentials, and execution settings reflect the resolved chain.
Profile Reference Semantics
Each profile may name at most one direct base. Bases may themselves inherit, so aliases and refinements can form a linear chain. Multiple inheritance and merging an array of profiles are outside this feature.
Every ID in a chain is resolved through the same composite catalog used for an ordinary profile selection. Existing precedence therefore applies separately to each lookup:
- in-memory profiles;
- the ordinary configured profile source;
- the application fallback profile source; and
- the embedded built-in catalog.
A higher-precedence definition of a base ID intentionally shadows a lower- precedence definition, just as it would if selected directly. References are not source-qualified, and there is no special syntax for bypassing an override to select a lower-precedence or specifically built-in definition. A profile cannot extend a shadowed definition with its own ID; that is a self-cycle.
Profile chains are resolved afresh for each ordinary preparation, execution, or inspection operation, preserving the current fresh-source behavior. The resolver does not cache a chain across operations. Mutable sources are not promised a transactional snapshot across separate file reads; callers that need a frozen result use prepared execution.
Override Semantics
Inheritance combines profile definitions from the root base to the selected
leaf. The leaf's id is always retained, and base_profile is resolution
metadata rather than an execution setting.
Child fields use the profile conventions already exposed by Promptkit:
- nonblank
backend,endpoint,model,service_tier,reasoning_effort, andapi_key_envvalues replace inherited values; - nonzero
temperature,max_tokens,top_p, andtimeout_secondsvalues replace inherited values; - omitted, blank, or zero fields inherit, according to the existing profile contract;
- a nonempty
extra_paramsmap replaces the complete inherited map rather than merging individual keys; and APIKeyRequired: trueis inherited and remains sticky through the chain. It retains the existing security behavior of clearing inherited profile or backend environment sources and requiring a direct request key or an explicit requestAPIKeyEnvoverride.
This feature does not introduce profile-level presence pointers or clearing syntax. A derived profile cannot use a blank string, numeric zero, false, or an empty map to clear an inherited value because those states already mean "unspecified" for profile configuration. Consumers can use the existing presence-aware runtime overrides when they need an explicit zero or an empty reasoning value. A future clearing syntax may be considered independently if real consumer demand emerges.
Connection fields retain their existing independent meanings. A child endpoint may override an inherited endpoint without changing backend identity, and a child backend replaces an inherited backend ID. Other fields continue to inherit unless the child supplies their ordinary nonblank or nonzero override.
Validation And Failure Behavior
Profile parsing and registration must distinguish local validity from resolved completeness:
- IDs, base IDs, supplied endpoints, numeric bounds, extra parameters, and other values that already have source-local rules remain validated at their owning input boundary.
- A standalone profile with no base continues to require a model and at least one backend or endpoint.
- A profile with a base may omit those required target fields, because the resolved chain may provide them.
- The completely merged profile must satisfy the same target invariants as a current standalone profile before backend resolution and use.
- In-memory profiles without a base retain their current
NewEnginevalidation behavior. Reference existence, cycles, and resolved completeness are evaluated when an inherited profile is selected or inspected, so file and in-memory profiles share one resolution contract and fresh lower sources are not frozen at engine construction.
Resolution must detect direct and indirect cycles and impose a maximum chain length of 32 profiles, including the selected leaf. The diagnostic should name the relevant profile chain without exposing internal package values.
Failure identity distinguishes the requested profile from its dependencies:
- an absent directly selected profile retains
ErrProfileNotFound; - an existing selected profile whose base is absent, malformed, cyclic,
excessively deep, or incomplete fails with
ErrProfileLoad, notErrProfileNotFound; and - backend lookup and resolved-setting failures retain their existing
ErrProfileLoadbehavior.
A malformed higher-precedence definition remains authoritative and stops fallback. Inheritance must not silently skip a broken base to use another definition or partially resolve a chain.
Architectural Ownership
Inheritance resolution belongs in internal/profile, around the fully
assembled composite repository. Individual filesystem, fs.FS, in-memory,
and built-in repositories continue to own discovery, strict decoding, local
normalization, defensive copying, and source precedence; they must not resolve
bases independently within their own source.
The root facade assembles the raw composite catalog and the resolving profile
boundary. internal/usecase continues to request one selected profile and
resolve its backend; it should receive an already combined, caller-owned
profile rather than implementing recursion or source traversal itself.
internal/domain may carry the normalized base ID and any internal information
needed to distinguish locally supplied fields, but those internal values must
not leak through public execution targets or provider requests. Final target
resolution still follows:
- framework defaults;
- the backend selected by the resolved profile;
- the fully resolved profile; and
- request runtime overrides.
Public And Format Surface
The feature adds only the smallest consumer-facing configuration needed for one-parent inheritance:
- YAML
base_profilein the profile format; Profile.BaseProfileID; andOpenAICompatibleProfileConfig.BaseProfileID.
Exact public behavior belongs in the new fields' GoDoc once implemented. The profile format reference owns YAML syntax, merge rules, chain limits, and validation behavior. Consumer guidance should show one concise alias/refinement workflow and link to those canonical contracts rather than duplicating them.
No new engine method, profile registry mutation API, public resolver, public
inheritance graph, or result provenance field is required. Existing profile
selection through prompt defaults and RunRequest.ProfileID remains unchanged.
Quality And Documentation End State
Testing should follow the repository's lean ownership model:
- profile parser and registration tests own the new field, local validation, and defensive copying;
- profile resolver tests own cross-source lookup, precedence, chain merging, cycle and depth protection, missing bases, and resolved completeness;
- use-case tests retain ownership of backend and runtime-override precedence without duplicating the full inheritance matrix; and
- one representative external-package workflow should prove that a downstream alias of a built-in profile reports the child ID and inherited effective target through preparation or inspection.
All tests remain deterministic, offline, parallel-safe, and independent of real provider credentials. Existing standalone-profile, overlay, malformed- selection, built-in, credential, and prepared-execution contracts must remain green.
The implemented feature's exact GoDoc, docs/formats.md,
docs/internal/sources.md, and focused consumer guidance must agree. The
internal component inventory reflects inheritance resolution as a concrete
internal/profile responsibility. Release documentation remains a separate
release-preparation concern.
Non-Goals
- Multiple inheritance or ordered profile mixins.
- Deep or key-by-key merging of
extra_params. - Source-qualified references or a special built-in namespace.
- Extending a shadowed lower-precedence profile with the same ID.
- Per-request changes to the base-profile relationship.
- Profile-level clearing syntax or presence-aware scalar fields.
- Caching resolved chains across operations or watching sources for changes.
- Changing backend registry, concurrency, credential, runtime-override, provider-wire, or prompt-selection semantics beyond applying them to the resolved profile.
- Consumer-specific configuration discovery, deployment migration, or routing policy.