Files
promptkit/docs/roadmap/profile-inheritance.md

225 lines
11 KiB
Markdown

# 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:
```yaml
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.FS` profile definitions may use the optional `base_profile`
field to name one parent profile.
- In-memory profiles expose the equivalent `Profile.BaseProfileID` field.
`OpenAICompatibleProfileConfig` exposes and forwards the same field so its
convenience constructor remains feature-complete.
- A profile containing only `id` and `base_profile` is 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`, and `InspectProfile` all 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:
1. in-memory profiles;
2. the ordinary configured profile source;
3. the application fallback profile source; and
4. 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`, and `api_key_env` values replace inherited values;
- nonzero `temperature`, `max_tokens`, `top_p`, and `timeout_seconds` values
replace inherited values;
- omitted, blank, or zero fields inherit, according to the existing profile
contract;
- a nonempty `extra_params` map replaces the complete inherited map rather
than merging individual keys; and
- `APIKeyRequired: true` is 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 request `APIKeyEnv` override.
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 `NewEngine`
validation 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`, not
`ErrProfileNotFound`; and
- backend lookup and resolved-setting failures retain their existing
`ErrProfileLoad` behavior.
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:
1. framework defaults;
2. the backend selected by the resolved profile;
3. the fully resolved profile; and
4. request runtime overrides.
## Public And Format Surface
The feature adds only the smallest consumer-facing configuration needed for
one-parent inheritance:
- YAML `base_profile` in the profile format;
- `Profile.BaseProfileID`; and
- `OpenAICompatibleProfileConfig.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.