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

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.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.