From f17a38f60b49d95e1fa398e24e7c3289b281c7e6 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Wed, 30 Sep 2026 03:34:15 +0000 Subject: [PATCH] Document keyed configuration and migrated consumer workflows --- README.md | 12 +- docs/consumers/pkg-promptkit.md | 58 +++ docs/formats.md | 559 ++++++++++++---------------- docs/roadmap/implementation.md | 2 + examples/go-library/prepare/main.go | 7 + 5 files changed, 312 insertions(+), 326 deletions(-) diff --git a/README.md b/README.md index 69668e7..15fcb56 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,8 @@ gitea.maximumdirect.net/eric/promptkit ``` The root `promptkit` package provides the supported public engine. Consumers -can configure filesystem or in-memory prompt, profile, and schema sources, +can configure file, embedded, or programmatic backend, profile, and prompt +definitions alongside filesystem or in-memory schemas, prepare requests without generation, run requests with the built-in OpenAI-compatible client, or inject their own model client and artifact reader. @@ -20,9 +21,12 @@ Run the maintained offline preparation example from the repository root: go run ./examples/go-library/prepare ``` -It loads a repository-local prompt, supplies an in-memory profile and inline -input, and prints deterministic preparation metadata without contacting a -provider or requiring credentials. Read the +It loads a repository-local prompt, supplies a programmatic profile and inline +input, inspects the captured configuration, and prints deterministic +preparation metadata without contacting a provider or requiring credentials. +The [consumer guide](docs/consumers/pkg-promptkit.md) also shows a local backend, +ordered file/embedded sources, programmatic mappings, and how to replace an +engine when source files change. Read the [example source](examples/go-library/prepare/main.go), the [Go package consumer guide](docs/consumers/pkg-promptkit.md), and the [framework format reference](docs/formats.md) to build a consumer workflow. diff --git a/docs/consumers/pkg-promptkit.md b/docs/consumers/pkg-promptkit.md index cd3a2ad..d160420 100644 --- a/docs/consumers/pkg-promptkit.md +++ b/docs/consumers/pkg-promptkit.md @@ -78,6 +78,32 @@ explicitly selected missing or unreadable root is an error. Each helper finds caller may enable imports on individual inputs. A nil `Sources.Main` is an empty main source, not another directory lookup. +For a programmatic document with explicit null or a prompt definition, use a +labeled mapping as an input. A mapping owns a snapshot of supported values: + +```go +document := configkit.Mapping("application configuration", map[string]any{ + "profiles": map[string]any{ + "local-summary": map[string]any{ + "backend": "local", "model": "example-model", + "temperature": nil, // Suppress an inherited provider control. + }, + }, +}) +input := configkit.Input{Source: document} +// Add input to a later underlay, or set Sources.Main = &input. +``` + +`ProfileSource` and `BackendSource` are smaller typed conveniences when null +is not needed. A caller underlay can overlay catalog backend fields just as it +can overlay profiles; there is no registration-only backend namespace. + +When configuration or message files change, build and validate a new engine, +then switch the application to that engine for subsequent work. Existing +engines and prepared handles retain their captured definitions; schema and +request artifact resources still follow their runtime lifecycle. Inspection +does not watch or reload sources automatically. + ## Inspect The Captured Configuration Use [`Engine.InspectConfiguration`](../../configuration_inspection.go) for an @@ -323,6 +349,38 @@ For programmatic profiles, use [`ProfileSource`](../../profiles.go) with keyed `ProfileConfig` values. Nil fields are omitted; pointers preserve explicit zero, false, and blank values. Raw `configkit.Mapping` expresses explicit null. +### Migrate From The Previous Configuration API + +Replace `Config.PromptDir` and `Config.ProfileDir` with selected ConfigKit +inputs in `Config.Sources`. The removed `WithPromptFS`, `WithPromptFile`, +`WithProfileFS`, `WithProfileFile`, `WithFallbackProfileFS`, `WithProfiles`, +and `WithBackend` options have no compatibility wrappers. Use +`DirectoryInputs` or `FilesystemInputs` for a strict-composition file group, +ConfigKit file inputs for individual files, and `ProfileSource`, +`BackendSource`, or `configkit.Mapping` for programmatic values. Backend IDs +are keys in configuration, not registrations. Choose caller underlays in +fallback-to-operator-to-programmatic order; use `Sources.Main` for one final +highest-priority input. + +Move embedded `id` and `version` fields into keyed `profiles` and `prompts` +maps; convert `base_profile` to ConfigKit `extends`. IDs and references are +exact literal keys, without trimming. A supplied zero, false, or empty string +is now distinguishable from omission; null can clear or suppress an inherited +field according to the [field table](../formats.md#defaults-and-overrides). +Maps merge recursively across inheritance and layers. `{}` does not clear an +inherited map: use null to clear the whole map. At request time, a literal +`extra_params["extends"]` is ordinary provider data, not an inheritance +instruction; a nonempty request ExtraParams map replaces configured params. +Changing a model or backend does not clear unrelated explicit fields, though +backend-derived defaults are applied after inheritance to the final backend. + +Invalid final definitions, including unused ones, now fail `NewEngine`; only +request-dependent failures remain at preparation or execution. Prompt text +and static templates are captured at construction, not automatically reloaded +between runs. Replace the engine to pick up changed definitions or message +files. The [format reference](../formats.md) owns exact field and presence +rules; the maintained examples show complete runnable workflows. + ### Choose A Profile Ownership Model Choose the profile source that matches the application's continuity needs: diff --git a/docs/formats.md b/docs/formats.md index 5c92d2c..6d8cf67 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -2,394 +2,309 @@ ## Purpose -This document is the canonical contract for Promptkit prompt-definition, -profile, and schema files. The [Go package consumer guide](consumers/pkg-promptkit.md) -explains how to select these sources and invoke the engine. The +This is the canonical reference for Promptkit's backend, profile, prompt, and +schema definitions. The [Go consumer guide](consumers/pkg-promptkit.md) shows +how to select sources and use an engine; the [OpenAI-compatible integration contract](integrations/openai-compatible-chat.md) -owns the resulting outbound wire behavior. +owns outbound wire behavior. [ConfigKit](https://gitea.maximumdirect.net/eric/ConfigKit) +owns the generic YAML grammar, imports, layering, inheritance, and diagnostics. +This reference describes what Promptkit does with the final effective values. -Prompt and profile sources recursively discover files ending in `.yaml` or -`.yml`. Each prompt-definition and profile file contains exactly one YAML -document; comments and trailing whitespace are allowed. YAML decoding is -strict: unknown fields are errors for the selected definition. Definitions are -selected by their YAML `id`, not their file name or directory. +A configuration document has keyed top-level `backends`, `profiles`, and +`prompts` mappings. Their keys are literal IDs, not filename-derived identities. +Prompt versions are a second keyed mapping. Keys and references must be +nonblank and remain exact: Promptkit does not trim or re-key them. Old embedded +`id`, `version`, and `base_profile` fields are invalid. Unknown fields are +errors. + +```yaml +backends: + local: + endpoint: http://localhost:8000/v1 + concurrency_limit: 2 +profiles: + local-summary: + backend: local + model: example-model +prompts: + meeting.summary: + "1.0.0": + default_profile: local-summary + messages: + - role: user + content: Summarize this note. + output: + format: text + validation_mode: none +``` + +Every effective backend, profile, and prompt definition is validated when +`NewEngine` constructs the engine, even if it is never selected. A profile may +be partial: an endpoint and model need only be complete after request overrides +are applied. A prompt's default profile similarly need not be runnable without +an overriding request. Missing request data, artifacts, schemas, credentials, +and provider work remain runtime concerns. + +The engine captures effective definitions, referenced message-file contents, +and static template syntax at construction. It does not reread configuration or +message files on ordinary runs; construct a new engine to observe edits. +Schema and request artifact reads still happen during preparation. Use +`InspectConfiguration` for an explicit effective-configuration or provenance +report, `InspectProfile` for backend-completed profile settings, and preparation +for request-specific rendered details. ## Prompt Definitions -A prompt definition describes inputs, Go-template messages, an optional -default profile, and an output contract. +The `prompts` mapping selects a literal prompt ID, then a string version key: ```yaml -id: meeting.summary -version: "1.0.0" -default_profile: local-summary -description: Summarize a synthetic meeting note. -session_id: '{{.session}}' -inputs: - - name: note - required: true - content_type: text/plain - description: Meeting note to summarize. -messages: - - role: system - content: Return a concise summary. - cache_control: - type: ephemeral - ttl: 1h - - role: user - content_file: ./summary.user.md -output: - format: markdown - validation_mode: basic - repair_attempts: 0 +prompts: + meeting.summary: + "1.0.0": + default_profile: local-summary + description: Summarize a synthetic meeting note. + session_id: '{{.session}}' + inputs: + - name: note + required: true + content_type: text/plain + description: Meeting note to summarize. + messages: + - role: system + content: Return a concise summary. + cache_control: + type: ephemeral + ttl: 1h + - role: user + content_file: ./summary.user.md + output: + format: markdown + validation_mode: basic + repair_attempts: 0 ``` -| Field | Required | Meaning | -| --- | --- | --- | -| `id` | yes | Non-empty prompt identifier used by `RunRequest.PromptID`. | -| `version` | yes | Non-empty version selected by an optional `RunRequest.PromptVersion`. | -| `default_profile` | no | Non-empty profile ID used when the request omits `ProfileID`. | -| `description` | no | Human-readable description. | -| `session_id` | no | Go template rendered from request variables and input helpers. | -| `inputs` | no | Declared input metadata. | -| `messages` | yes | One or more chat-message templates. | -| `output` | yes | Output format and validation settings. | - -When a request omits a version, the selected prompt ID must identify exactly -one definition. When it supplies a version, the ID and version pair must be -unique. - -Exact prompt inspection uses this same configured source, strict decoding, -referenced content-file resolution, and ID/version selection. It reports the -selected definition's declared metadata without changing the prompt format or -executing the definition. +`messages` must be nonempty and `output` must be a valid mapping. Optional +`default_profile`, `description`, `session_id`, `inputs`, and cache-control +fields may be absent or cleared with null where appropriate. A null whole +definition, required role, output format, or validation mode is invalid. +`required` and `repair_attempts` default to false and zero, respectively, and +are not nullable. A request that omits a prompt version succeeds only if that +prompt ID has exactly one effective version. `InspectPrompt` selects from the +same captured definitions without rendering or resolving a profile. ### Inputs -Each `inputs` item has these fields: - -| Field | Required | Meaning | -| --- | --- | --- | -| `name` | yes | Non-empty name used by the request input map and `input` template helper. Names must be unique. | -| `required` | no | When true, preparation fails if the request omits the input. The default is false. | -| `content_type` | no | Expected media-type metadata. | -| `description` | no | Human-readable input description. | - -Requests supply inputs as inline or file-backed `ArtifactRef` values. Declared -required inputs must be present. A template reference also requires the named -input to exist, whether or not it was declared. Extra request inputs are -allowed. +Each `inputs` item may contain `name`, `required`, `content_type`, and +`description`. `name` is required, nonblank, and unique within the prompt. +Omitted `required` means false. Null `inputs` means no declarations. Requests +supply inline or file-backed `ArtifactRef` values; declared required inputs +must be present. A template reference also needs its named input, even when +undeclared. Extra request inputs are allowed. ### Messages And Templates -Each message has a `role` that Promptkit trims and lowercases. It must then be -exactly one of `developer`, `system`, `user`, or `assistant`; blank, custom, -`tool`, and `function` roles are invalid. This intentionally tightens the -previous nonblank-string rule. Consumers migrating to the next minor release -must update any nonstandard prompt-definition roles before upgrading. +Each message needs a role and exactly one nonnull `content` or `content_file`. +An explicitly empty inline `content` is valid. Null clears either content +selection. Roles are trimmed and lowercased, then must be `developer`, +`system`, `user`, or `assistant`. Static template syntax is checked during +construction; missing variables and named inputs fail when rendered. +Request variables are template data (`{{.audience}}`), and +`{{input "note"}}` renders an input body. -Each message also has exactly one of: +`content_file` uses the effective field's own source context, including when +inherited or imported. Its relative path is resolved against that source's +base directory within its rooted filesystem. Absolute paths and lexical +escapes from the selected root fail. Host-file sources also prevent symlink +escapes. Promptkit preserves the path text, including meaningful whitespace, +after checking that it is not blank. A source without a unique usable resource +context must supply one explicitly or override the field. Content is captured +at construction, not reopened for each request. -- `content`, containing an inline Go template; or -- `content_file`, naming a file whose contents are the Go template. - -`content_file` must be a relative path. It resolves from the directory that -contains the prompt file and must remain within the configured prompt source -root; parent components are allowed only when the resolved target remains -inside that root. Absolute paths and paths that escape the root are rejected. -Operating-system directory and single-file sources also reject symlink targets -outside the root, while injected `fs.FS` sources apply containment in that -filesystem's relative path namespace. For `WithPromptFile`, the source root is -the directory containing the selected prompt file. Promptkit uses the parsed -path text exactly after checking separately that it is not blank, so leading -and trailing whitespace can name real filesystem entries. - -Request variables are the template data, so a variable named `audience` is -referenced as `{{.audience}}`. The `{{input "note"}}` helper renders the body -of a named input. Missing variables and input references are errors. - -The optional `session_id` uses the same template data and input helper. Its -rendered value is trimmed, omitted when empty, and limited to 256 Unicode code -points. A nonblank direct request session ID bypasses this template completely; -a blank direct value leaves the template behavior unchanged. +Optional `session_id` uses the same template variables and input helper. Its +rendered value is trimmed, omitted if empty, and limited to 256 Unicode code +points. A nonblank direct request session ID bypasses the template. ### Cache Control -`cache_control` is optional and has these fields: - -| Field | Required | Values | -| --- | --- | --- | -| `type` | yes | `ephemeral` | -| `ttl` | no | Empty or `1h` | - -Promptkit preserves cache-control metadata on the rendered message. The -outbound integration determines its wire representation. +Optional `cache_control` has required `type: ephemeral` and optional `ttl`, +which may be empty or `1h`. Promptkit retains this metadata on rendered +messages; the integration contract defines its wire representation. ### Output Contract | Field | Required | Values or behavior | | --- | --- | --- | -| `format` | yes | `text`, `markdown`, or `json`. | -| `validation_mode` | yes | `none`, `basic`, `json`, or `json_schema`. | -| `schema_path` | for `json_schema` | Path to a schema in the configured schema source. | -| `repair_attempts` | no | Integer from zero through three; omitted means zero. A positive value requires `basic`, `json`, or `json_schema` validation. | +| `format` | yes | `text`, `markdown`, or `json` | +| `validation_mode` | yes | `none`, `basic`, `json`, or `json_schema` | +| `schema_path` | for `json_schema` | Path in the selected schema source | +| `repair_attempts` | no | Integer 0–3; default 0. Positive values need `basic`, `json`, or `json_schema` | -The validation modes behave as follows: - -- `none` skips content validation; -- `basic` requires non-empty generated content; -- `json` requires valid JSON; and -- `json_schema` requires valid JSON that satisfies the selected schema. - -`format` controls output artifact metadata. JSON Schema mode also supplies the -schema to compatible model clients as structured-output metadata. Plain `json` -validation accepts every valid JSON value and does not request a provider-native -JSON-object constraint. - -`repair_attempts` counts additional generation calls after a failed validation. -Zero is single-pass. With a positive eligible budget, Promptkit stops at the -first valid candidate. If the budget is exhausted, it returns the final -candidate and its complete failed validation result; generation and operational -validation failures remain errors. `none` never permits repair. - -A request-level `OutputContract` replaces the complete prompt output contract. -It does not merge individual fields. If its format is empty, Promptkit uses -`text`. +`none` skips content validation; `basic` requires nonempty content; `json` +accepts any valid JSON value; `json_schema` checks the selected schema. +`format` determines output artifact metadata. JSON Schema mode also supplies +structured-output metadata to compatible clients. Plain `json` does not +request a provider-native JSON-object constraint. A positive repair budget +counts additional generations after failed validation; exhaustion returns the +last candidate and its failed validation result. Operational errors remain +errors. A request-level `OutputContract` replaces the whole configured one, +not individual fields; an empty request format defaults to `text`. ## Built-In Backends -Every engine provides these reserved OpenAI-compatible backend IDs. Consumers -must not register either ID with `WithBackend`; exact registration and -reservation behavior belongs to the [`Backend` GoDoc](../backends.go). +The selected catalog modules contribute these backend definitions as the +earliest underlay. Their settings can be overlaid by caller sources; these IDs +are not reserved against configuration changes. -| ID | Base endpoint | API-key environment variable | Active generation limit | Default queue capacity | -| --- | --- | --- | ---: | ---: | -| `openrouter` | `https://openrouter.ai/api/v1` | `OPENROUTER_API_KEY` | 16 | 1024 | -| `rakestrawhome` | `https://inference.ai.rakestrawhome.com/v1` | `RAKESTRAWHOME_INFERENCE_API_KEY` | 4 | 1024 | +| ID | Catalog endpoint | API-key environment name | Active generation limit | +| --- | --- | --- | ---: | +| `openrouter` | `https://openrouter.ai/api/v1` | `OPENROUTER_API_KEY` | 16 | +| `rakestrawhome` | `https://inference.ai.rakestrawhome.com/v1` | `RAKESTRAWHOME_INFERENCE_API_KEY` | 4 | + +A backend definition may have `endpoint`, `api_key_env`, +`concurrency_limit`, `queue_capacity`, and `extra_params`. The final endpoint +is required and must be an absolute HTTP(S) URL with a host, no user +information, query, or fragment. An omitted or null API-key environment name +means no lookup. Omitted/null concurrency is zero (unlimited); a supplied +negative value is invalid. For positive concurrency, omitted/null queue +capacity defaults to 1024. An explicit queue capacity, including zero, +requires positive concurrency and must be nonnegative. Unlimited concurrency +has no queue. Provider maps contain JSON-compatible values; null entries are +ordinary JSON null. Unknown fields, including a raw `api_key`, fail. ## Profile Definitions -A profile supplies model execution settings: +Profiles are keyed by literal ID and may inherit a named parent with +`extends`: ```yaml -id: local-summary -backend: openrouter -model: example-model -temperature: 0.2 -max_tokens: 500 -top_p: 0.95 -timeout_seconds: 90 -service_tier: flex -reasoning_effort: medium -extra_params: - provider_option: enabled +profiles: + local-summary: + backend: local + model: example-model + temperature: 0.2 + max_tokens: 500 + timeout_seconds: 90 + extra_params: + provider_option: enabled + local-summary-fast: + extends: local-summary + timeout_seconds: 30 + reasoning_effort: low ``` -A derived profile can use a named base and override only the settings it owns: - -```yaml -id: local-summary-fast -base_profile: local-summary -timeout_seconds: 30 -reasoning_effort: low -``` - -| Field | Required | Meaning | -| --- | --- | --- | -| `id` | yes | Profile identifier, trimmed before selection and publication. It must be non-empty after trimming and unique within one source after normalization. | -| `base_profile` | no | One optional parent profile ID. A derived profile may inherit target fields from it. | -| `backend` | unless `endpoint` is present | Backend registry ID. It is trimmed and registry membership is checked when the profile is prepared or inspected. | -| `endpoint` | unless `backend` is present | OpenAI-compatible base URL, including an API version path when required. A nonempty value is trimmed and must be absolute HTTP or HTTPS with a host and without user information, a query, or a fragment. When both connection fields are present, this overrides the backend endpoint without changing backend identity. | -| `model` | yes | Non-empty provider model name. | -| `temperature` | no | Number from 0 through 2. | -| `max_tokens` | no | Integer zero or greater. | -| `top_p` | no | Number from 0 through 1. | -| `timeout_seconds` | no | Per-generation deadline in whole seconds; integer zero or greater. | -| `service_tier` | no | Provider-specific request tier. | -| `reasoning_effort` | no | Provider-specific reasoning setting. | -| `api_key_env` | no | Optional environment-variable lookup source for an API key. | -| `extra_params` | no | JSON-compatible provider-specific outbound fields. | - -Raw `api_key` is prohibited in profile YAML. Store only an environment -variable name in `api_key_env`. - -A standalone profile must provide a model and at least one of `backend` or -`endpoint`. A derived profile may omit those target fields because its selected -base chain can provide them. Local parsing still validates a derived profile's -own ID, supplied endpoint, execution-setting bounds, and `extra_params`. - -Promptkit does not infer a backend from a model or endpoint. Endpoint-only -profiles remain supported and have no effective backend ID. -The engine always provides the built-in `openrouter` and `rakestrawhome` IDs. -Consumers can add engine-scoped IDs with -[`WithBackend`](../backends.go); exact registration validation belongs to its -GoDoc. - -`extra_params` accepts null, booleans, finite numbers, strings, arrays, and -objects with string keys. Keys must be non-empty. With the built-in client, -they also cannot collide with the standard fields listed in the -[outbound request contract](integrations/openai-compatible-chat.md#request-body). -Excessively deep or large JSON-shaped values are rejected for safety. +Profiles may also supply `endpoint`, `top_p`, `service_tier`, `api_key_env`, +and `api_key_required`. Promptkit does not infer a backend from an endpoint or +model. A profile with only an endpoint has no backend capacity identity. +Nonblank endpoints follow the backend URL rules; temperature is 0–2, top_p +is 0–1, max_tokens and timeout_seconds are nonnegative. Provider maps accept +JSON-compatible values with nonempty keys and cannot collide with the built-in +client's [standard request fields](integrations/openai-compatible-chat.md#request-body). +Credential values do not belong in profiles: raw `api_key` is invalid. ### Defaults And Overrides -Execution settings resolve in this order: +After effective profile inheritance, Promptkit applies the selected backend's +defaults once, then request `ExecutionTargetOverride` values. Backend-derived +settings are not inheritance input: changing a child profile's backend does +not carry the parent's derived endpoint or credential source into it. Changing +only a model or backend does not clear unrelated explicitly inherited fields. -1. the framework timeout baseline; -2. the selected backend, when the profile names one; -3. the selected profile; and -4. request `ExecutionTargetOverride` values. +| Profile field | Absent | Explicit value | Explicit null | +| --- | --- | --- | --- | +| `backend` | No backend | Exact backend ID; blank means none | No backend | +| `endpoint` | Selected backend endpoint | Valid nonblank URL; empty clears | Clear, without backend fallback | +| `model` | Unspecified | Nonblank model; empty clears | Unspecified | +| `temperature`, `top_p`, `max_tokens` | Unspecified | Valid value, including outbound zero | Suppress outbound control | +| `service_tier`, `reasoning_effort` | Unspecified | Normalized value; blank suppresses | Suppress outbound control | +| `timeout_seconds` | 600 seconds | Nonnegative; zero disables generation deadline | Reset to 600 seconds | +| `api_key_env` | Backend lookup name, if any | Trimmed name; blank disables | Disable backend lookup | +| `api_key_required` | False | Boolean, including false | False | +| `extra_params` | Backend defaults | Recursively overlay profile map on backend map | Suppress configured provider map | -The framework baseline is: - -| Setting | Default | -| --- | --- | -| `temperature` | Unspecified and omitted from compatible provider requests unless a profile or runtime override selects it. | -| `max_tokens` | Unspecified and omitted from compatible provider requests unless a profile or runtime override selects it. | -| `top_p` | Unspecified and omitted from compatible provider requests unless a profile or runtime override selects it. | -| `timeout_seconds` | `600` | - -Numeric zero in a file or in-memory profile does not select a numeric value. -For `temperature`, `max_tokens`, and `top_p`, it leaves the provider control -unspecified. For `timeout_seconds`, it retains the framework deadline. Numeric -request overrides use pointers, so an explicit zero is retained and sent to -compatible providers. In particular, an explicit request `timeout_seconds` of -zero disables the per-generation deadline while leaving the caller context and -transport timeout intact. - -Non-empty profile strings replace backend defaults, and non-empty request -strings replace both. Request reasoning is the exception: a nil -`ReasoningEffort` pointer inherits the profile, a pointer to a nonblank string -trims and replaces it, and a pointer to a blank string clears it. Backend -identity is retained when either layer overrides the endpoint, so the override -also retains any engine-local capacity policy configured for that backend. -Capacity configuration belongs to the Go -[`Backend` API](../backends.go), not prompt or profile YAML. A non-empty -`extra_params` map at each layer replaces the entire lower-precedence map -rather than merging keys. -The [outbound integration contract](integrations/openai-compatible-chat.md) -defines how the effective settings are serialized. +An empty mapping does not clear inherited map entries; null clears the whole +map. Nested maps merge recursively, while scalar and array values replace. +Runtime nonempty `ExtraParams` overrides replace the entire configured map. +Request overrides otherwise follow the [public value GoDoc](../types.go). +For example, a runtime `extra_params["extends"]` key is ordinary provider +data, not a configuration inheritance instruction. An explicit zero timeout +does not disable the caller context or HTTP transport timeout. ### Source And Profile Precedence -An explicit request profile ID takes precedence over the prompt's -`default_profile`. If neither is present, preparation fails. Exact profile -inspection instead takes one explicit profile ID and does not use a prompt -default. +`NewEngine` composes catalog definitions first, Promptkit-owned definitions +next, then caller underlays in their given order, with an optional main input +last. Later tiers overlay earlier ones. Inputs within one underlay group are +strictly composed: conflicting contributions are errors, not priority +signals. The directory helpers discover `.yaml` and `.yml` files recursively +in sorted order, but that order does not give one file higher precedence. +An explicitly selected missing/unreadable root fails; an empty root contributes +nothing. Imports are disabled by these helpers unless the caller enables them +on an input. Single files can use ConfigKit inputs directly. -Profile sources resolve matching IDs in this order: - -1. in-memory profiles supplied with `WithProfiles`; -2. the ordinary configured source selected by a profile file, `fs.FS`, or - configured profile directory; -3. application fallback profiles supplied with `WithFallbackProfileFS`; and -4. Promptkit-owned profiles; and -5. maintained external catalog profiles. - -A profile source supplies a complete definition; definitions and their fields -are not merged across sources. A higher-precedence source falls back only when -the requested profile ID is absent. An invalid matching profile is an error and -does not fall back. In-memory `Profile` values follow the same ranges as YAML -profiles. They use `APIKeyRequired` for request-scoped credentials instead of -`api_key_env`. Preparation and exact profile inspection use this same source -precedence. - -When a selected definition names `base_profile`, every profile ID in that -chain is looked up through this same precedence order. A higher-precedence -definition therefore shadows a lower-precedence definition of the same base -ID, including a built-in. References are not source-qualified. +Backends, profiles, and prompts all follow these same source rules. A later +profile or backend can change individual fields of a lower definition. A +request `ProfileID` takes precedence over a prompt `default_profile`; if +neither is selected, preparation fails. Exact profile inspection selects one +explicit ID and does not consult a prompt default. ### Profile Inheritance -Promptkit resolves one linear base chain of at most 32 profiles, including the -selected profile. It merges settings from the root base to the selected leaf. -The leaf's `id` remains the selected profile identity. Nonblank string fields -(`backend`, `endpoint`, `model`, `service_tier`, `reasoning_effort`, and -`api_key_env`) and nonzero numeric fields replace inherited values. A nonempty -`extra_params` map replaces the complete inherited map rather than merging -keys, and `APIKeyRequired: true` remains true through the chain. Backend and -endpoint are independent: replacing one does not clear the other. - -There is no profile-level clearing syntax. Blank strings, zero numbers, false, -and empty maps remain unspecified and inherit from a base. Use existing -presence-aware request overrides where an execution needs an explicit zero or -empty reasoning setting. - -An absent directly selected profile reports the ordinary not-found error. Once -the selected profile exists, a missing base, cycle, overlong chain, or -incomplete resolved target is a profile-load failure. Ordinary operations -resolve chains afresh; prepared execution retains the fully resolved target. +ConfigKit resolves `extends` on effective profile definitions. One profile +may inherit another keyed profile; presence, explicit null, and recursively +merged maps determine the final fields. References are literal IDs, including +IDs containing punctuation. There is no Promptkit-specific depth limit or +second base-chain resolver. Missing references and cycles fail at construction. +For generic inheritance and layer mechanics, consult +[ConfigKit](https://gitea.maximumdirect.net/eric/ConfigKit). ## Built-In Profile Catalog -Every built-in profile selects one maintained built-in backend and inherits -that backend's connection and credential metadata. Profile files do not repeat -those values. A configured, application fallback, or in-memory profile with -the same profile ID takes precedence. - -Promptkit owns two opt-in profiles. The following definitions are current -selections, not automatic defaults: +Promptkit contributes two opt-in definitions; neither is selected by default: | ID | Current definition | | --- | --- | -| `promptkit-openrouter-standard` | Inherits `gemini-flash-latest`. | +| `promptkit-openrouter-standard` | Extends the selected OpenRouter catalog's `gemini-flash-latest` profile. | | `promptkit-openrouter-auto` | Selects backend `openrouter` and model `openrouter/auto`. | -Select either name through an existing request `profile_id` or prompt -`default_profile`. Consumers can replace either definition or its inherited -base through the ordinary profile-source precedence above. The -[release policy](policy/release.md#catalog-profile-compatibility) defines the -cross-version guarantee for these names. +Select one through request `ProfileID` or prompt `default_profile`. +Caller layers may overlay these definitions or their catalog bases. The +[release policy](policy/release.md#catalog-profile-compatibility) owns their +cross-version compatibility boundary. ## External Profile Inventories -The external profile inventory is defined by the selected catalog modules, not -duplicated here. Browse the exact data selected by this Promptkit version: +The selected catalog modules, not this document, own their profile inventories: -- [OpenRouter catalog v2.20260926.0 profiles](https://gitea.maximumdirect.net/eric/promptkit-backend-openrouter/src/tag/v2.20260926.0/catalog/profiles) -- [Rakestrawhome catalog v2.20260926.0 profiles](https://gitea.maximumdirect.net/eric/promptkit-backend-rakestrawhome/src/tag/v2.20260926.0/catalog/profiles) +- [OpenRouter catalog v3.20260930.0 profiles](https://gitea.maximumdirect.net/eric/promptkit-backend-openrouter/src/tag/v3.20260930.0/catalog/profiles) +- [Rakestrawhome catalog v3.20260930.0 profiles](https://gitea.maximumdirect.net/eric/promptkit-backend-rakestrawhome/src/tag/v3.20260930.0/catalog/profiles) -External profile entries are volatile across catalog releases. The -[release policy](policy/release.md#catalog-profile-compatibility) owns the precise -compatibility boundary; a selected module version identifies tested local data, -not live provider availability. +These are tested local data, not a promise of live provider availability. +The [release policy](policy/release.md#catalog-profile-compatibility) owns +the compatibility boundary. ## Schemas -Schemas are JSON documents selected by a prompt or request -`schema_path`. For a directory or `fs.FS` source, paths resolve within the -configured source root. Referenced nested schemas resolve relative to the -owning schema document. `WithSchemaFile` exposes one schema, addressed by its -base name. - -An unreadable, invalid, or unresolvable schema produces an operational -validation error. Generated content that is valid JSON but does not satisfy the -schema produces a failed validation result. +Schemas are JSON documents selected by a prompt or request `schema_path`. +Directory and `fs.FS` schema paths resolve within the configured root; +referenced nested schemas resolve relative to the owning schema document. +`WithSchemaFile` exposes one schema under its base name. An unreadable, +invalid, or unresolvable schema causes an operational validation error. +Valid JSON that fails the schema instead yields a failed validation result. ## Credentials -Credential values belong at the request or environment boundary, never in -prompt, profile, schema, or example files: +Only environment-variable names belong in backend/profile definitions, never +credential values. A request may supply a direct `APIKey` or an `APIKeyEnv` +override. A direct key takes precedence, followed by a request environment +source, an explicit profile source, and an inherited backend source. A true +profile `api_key_required` suppresses the backend-derived environment source, +but an explicit profile or request environment source remains usable. False +restores ordinary optional-credential behavior. Missing or blank optional +environment values leave the built-in client's Authorization header absent; +required availability is checked at preparation and again during prepared +execution. Injected clients own their credential-resolution behavior. -- a backend or file profile can name an optional environment lookup source - with `APIKeyEnv` or `api_key_env`; -- an in-memory profile may set `APIKeyRequired` as an explicit local - requirement; -- a request can provide a direct `APIKey` or override the optional `APIKeyEnv` - source; and -- a direct request key takes precedence over environment lookup. - -After a direct request key, the credential-source precedence is request -`APIKeyEnv`, profile `api_key_env`, then the backend default. An in-memory -profile with `APIKeyRequired` clears an inherited backend environment name and -requires a direct key unless the request explicitly supplies `APIKeyEnv`. -Named environment sources are optional: when the selected source is absent, -empty, or whitespace-only, the built-in client omits the `Authorization` -header and handles the provider response normally. `APIKeyRequired` is the -only explicit local availability requirement. Promptkit validates required -credential availability during preparation and rechecks it when a prepared -execution runs. Injected clients receive resolved source metadata but define -their own credential-resolution behavior. -Direct keys are excluded from JSON results and redacted by public string -formatters. Environment-variable names may appear in prepared metadata, but -their values do not. +Credential values are excluded from JSON results and redacted by public +string formatters. ConfigKit inspection redacts credential-shaped dynamic +provider keys, but does not make arbitrary prompt/provider content safe to +disclose. Applications own report disclosure and secret handling. diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index b95feb0..d355a6e 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -543,6 +543,8 @@ The runner has no reference to the inspector or ConfigKit metadata types. ## Stage 10 — Migrate Examples And Canonical Format Guidance +**Completed:** 2026-09-30. + Scope: maintained examples and user-facing documentation. 1. Complete the preparation, execution, and observation example migration begun diff --git a/examples/go-library/prepare/main.go b/examples/go-library/prepare/main.go index 58ef722..b2d14aa 100644 --- a/examples/go-library/prepare/main.go +++ b/examples/go-library/prepare/main.go @@ -29,6 +29,13 @@ func main() { if err != nil { exit(err) } + view, err := engine.InspectConfiguration() + if err != nil { + exit(err) + } + if _, found := view.Explain(configkit.Keys("profiles", "offline-example", "model")); !found { + exit(fmt.Errorf("configured profile model was not found")) + } prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{ PromptID: "example.prepare",