225 lines
11 KiB
Markdown
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.
|