diff --git a/backends.go b/backends.go index 33774fd..4343a98 100644 --- a/backends.go +++ b/backends.go @@ -19,10 +19,13 @@ const BackendLocal = "local" // explicit zero or blank values. A nil ExtraParams map is omitted; a non-nil // empty map supplies an empty mapping. A nil SmokeTests pointer is omitted; a // non-nil pointer supplies a smoke_tests mapping. Use raw ConfigKit Mapping -// inputs for explicit null. BackendConfig has no stable JSON representation. +// inputs for explicit null. Extends names a sibling backend in the same list. +// BackendConfig has no stable JSON representation. type BackendConfig struct { // ID is the local backend identity within its resource list. ID string + // Extends names a sibling backend to inherit through ConfigKit. + Extends *string // Endpoint is the required absolute HTTP(S) base URL after all layers merge. Endpoint *string // APIKeyEnv optionally names an environment variable, not a credential. @@ -84,6 +87,9 @@ func BackendSource(label string, backends []BackendConfig) configkit.Source { definitions := make([]any, 0, len(backends)) for _, config := range backends { fields := map[string]any{"id": config.ID} + if config.Extends != nil { + fields["extends"] = map[string]any{"id": *config.Extends} + } if config.Endpoint != nil { fields["endpoint"] = *config.Endpoint } diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index dd5f2c5..074864d 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -470,6 +470,10 @@ Do not introduce benchmark thresholds or additional release gates. ## Stage 9: Add Typed Backend Inheritance And Prompt Sources +**Completed:** Backend inheritance and the typed prompt mapping constructor +follow the existing ConfigKit source pattern. Existing focused programmatic +source and inspection tests pass; composition coverage follows next. + **Scope:** Public mapping-constructor implementation and GoDoc. 1. Add `Extends *string` to `BackendConfig` and encode it in `BackendSource` diff --git a/prompts.go b/prompts.go new file mode 100644 index 0000000..982b7e5 --- /dev/null +++ b/prompts.go @@ -0,0 +1,129 @@ +package promptkit + +import "go.maximumdirect.net/configkit" + +// PromptConfig is a presence-aware source input for one prompt. Extends names +// a sibling prompt. Nil optional pointers and slices are omitted; non-nil +// pointers and slices preserve explicit zero, blank, and empty values. These +// inputs are mapping constructors, not serialized inspection values. Raw +// ConfigKit mappings can express explicit null and other composition controls. +type PromptConfig struct { + ID string + Extends, DefaultProfile, Description, SessionID *string + Inputs []PromptInputConfig + Messages []PromptMessageConfig + Output *PromptOutputConfig +} + +// PromptInputConfig supplies an input declaration. Name is always supplied; +// nil optional pointers omit their fields. +type PromptInputConfig struct { + Name string + Required *bool + ContentType, Description *string +} + +// PromptMessageConfig supplies message fields, including inline content or a +// content-file reference. Final content selection is validated by NewEngine. +type PromptMessageConfig struct { + Role, Content, ContentFile *string + CacheControl *PromptCacheControlConfig +} + +// PromptCacheControlConfig supplies optional cache-control source fields. +type PromptCacheControlConfig struct{ Type, TTL *string } + +// PromptOutputConfig supplies optional output contract source fields. +type PromptOutputConfig struct { + Format, ValidationMode, SchemaPath *string + RepairAttempts *int +} + +// PromptSource creates a labeled ConfigKit mapping in caller order. Nil +// top-level input supplies an empty prompts sequence. Records may be partial: +// sibling inheritance and layered sources can complete them before NewEngine +// validates the final configuration. Mapping snapshots supported values when +// constructed; the helper neither resolves inheritance nor reads files. +// Relative content files use the ConfigKit Input filesystem context of the +// contributing field. Raw mappings remain available for explicit null and +// generic ConfigKit operations. +func PromptSource(label string, prompts []PromptConfig) configkit.Source { + definitions := make([]any, 0, len(prompts)) + for _, config := range prompts { + fields := map[string]any{"id": config.ID} + if config.Extends != nil { + fields["extends"] = map[string]any{"id": *config.Extends} + } + if config.DefaultProfile != nil { + fields["default_profile"] = *config.DefaultProfile + } + if config.Description != nil { + fields["description"] = *config.Description + } + if config.SessionID != nil { + fields["session_id"] = *config.SessionID + } + if config.Inputs != nil { + inputs := make([]any, 0, len(config.Inputs)) + for _, input := range config.Inputs { + item := map[string]any{"name": input.Name} + if input.Required != nil { + item["required"] = *input.Required + } + if input.ContentType != nil { + item["content_type"] = *input.ContentType + } + if input.Description != nil { + item["description"] = *input.Description + } + inputs = append(inputs, item) + } + fields["inputs"] = inputs + } + if config.Messages != nil { + messages := make([]any, 0, len(config.Messages)) + for _, message := range config.Messages { + item := make(map[string]any) + if message.Role != nil { + item["role"] = *message.Role + } + if message.Content != nil { + item["content"] = *message.Content + } + if message.ContentFile != nil { + item["content_file"] = *message.ContentFile + } + if message.CacheControl != nil { + cache := make(map[string]any) + if message.CacheControl.Type != nil { + cache["type"] = *message.CacheControl.Type + } + if message.CacheControl.TTL != nil { + cache["ttl"] = *message.CacheControl.TTL + } + item["cache_control"] = cache + } + messages = append(messages, item) + } + fields["messages"] = messages + } + if config.Output != nil { + output := make(map[string]any) + if config.Output.Format != nil { + output["format"] = *config.Output.Format + } + if config.Output.ValidationMode != nil { + output["validation_mode"] = *config.Output.ValidationMode + } + if config.Output.SchemaPath != nil { + output["schema_path"] = *config.Output.SchemaPath + } + if config.Output.RepairAttempts != nil { + output["repair_attempts"] = *config.Output.RepairAttempts + } + fields["output"] = output + } + definitions = append(definitions, fields) + } + return configkit.Mapping(label, map[string]any{"prompts": definitions}) +}