Preserve message resource origins and profile credential lookup names

This commit is contained in:
2026-10-03 01:21:48 +00:00
parent 8c68b28421
commit 62a4a20529
7 changed files with 118 additions and 39 deletions

View File

@@ -275,9 +275,6 @@ func validateEffective(config *document, validation *configkit.Validation) {
if profile.Model != nil && *profile.Model != "" && strings.TrimSpace(*profile.Model) == "" {
validation.Error(base.Key("model"), "model must not be blank")
}
if profile.APIKeyEnv != nil && *profile.APIKeyEnv != "" && !envName.MatchString(*profile.APIKeyEnv) {
validation.Error(base.Key("api_key_env"), "environment variable name is invalid")
}
if profile.Temperature != nil {
if err := domain.ValidateExecutionTargetSettings(domain.ExecutionTarget{Temperature: *profile.Temperature}); err != nil {
validation.Error(base.Key("temperature"), err.Error())

View File

@@ -66,37 +66,9 @@ func resolvePrompts(prompts map[string]map[string]promptConfig, resolution *conf
if inputsChanged {
resolution.Record(base.Key("inputs"), configkit.ChangeNormalization, "normalize input metadata")
}
messagesChanged := false
for index := range definition.Messages {
message := &definition.Messages[index]
messagePath := base.Key("messages").Index(index)
if message.Role != nil {
role, err := domain.NormalizeMessageRole(*message.Role)
if err != nil {
resolution.Error(messagePath.Key("role"), err.Error())
} else {
messagesChanged = normalizeString(&message.Role, messagePath.Key("role"), resolution, role) || messagesChanged
}
}
if control := message.CacheControl; control != nil && control.Type != nil {
value := &domain.CacheControl{Type: domain.CacheControlType(*control.Type)}
if control.TTL != nil {
value.TTL = *control.TTL
}
normalized, err := domain.NormalizeCacheControl(value)
if err != nil {
resolution.Error(messagePath.Key("cache_control"), err.Error())
} else {
messagesChanged = normalizeString(&control.Type, messagePath.Key("cache_control").Key("type"), resolution, string(normalized.Type)) || messagesChanged
if control.TTL != nil {
messagesChanged = normalizeString(&control.TTL, messagePath.Key("cache_control").Key("ttl"), resolution, normalized.TTL) || messagesChanged
}
}
}
}
if messagesChanged {
resolution.Record(base.Key("messages"), configkit.ChangeNormalization, "normalize message metadata")
}
// Normalize message metadata on execution-owned copies below. ConfigKit
// records sequence mutations as whole-value replacements, which would
// discard the effective source contexts of unchanged content_file fields.
if definition.Output != nil && definition.Output.SchemaPath != nil {
normalizeString(&definition.Output.SchemaPath, base.Key("output").Key("schema_path"), resolution, strings.TrimSpace(*definition.Output.SchemaPath))
}
@@ -180,6 +152,10 @@ func (state *loaded) materializePrompt(id, version string, raw promptConfig, loo
if message.Role == nil {
return nil, fmt.Errorf("%s: role is required", path.Key("role"))
}
role, err := domain.NormalizeMessageRole(*message.Role)
if err != nil {
return nil, fmt.Errorf("%s: %w", path.Key("role"), err)
}
if (message.Content != nil) == (message.ContentFile != nil) {
return nil, fmt.Errorf("%s: exactly one of content or content_file is required", path)
}
@@ -214,7 +190,7 @@ func (state *loaded) materializePrompt(id, version string, raw promptConfig, loo
}
entry.messages = append(entry.messages, compiled)
definition.Templates = append(definition.Templates, domain.PromptMessageTemplate{
Role: *message.Role, Content: content, CacheControl: control,
Role: role, Content: content, CacheControl: control,
})
}
if raw.Output == nil {

View File

@@ -15,7 +15,7 @@ import (
func TestLoadCapturesInheritedImportedContent(t *testing.T) {
files := fstest.MapFS{
"app/main.yaml": {Data: []byte("configkit:\n imports:\n - path: ../catalog/base.yaml\nprompts:\n child:\n extends: base\n")},
"catalog/base.yaml": {Data: []byte("prompts:\n base:\n v1:\n messages:\n - role: user\n content_file: ./body.md\n output:\n format: text\n validation_mode: basic\n")},
"catalog/base.yaml": {Data: []byte("prompts:\n base:\n v1:\n messages:\n - role: ' USER '\n cache_control:\n type: ' ephemeral '\n ttl: ' 1h '\n content_file: ./body.md\n output:\n format: text\n validation_mode: basic\n")},
"catalog/body.md": {Data: []byte("Captured {{.name}}")},
}
namespace := configkit.NewFilesystem("embedded", files)
@@ -37,6 +37,23 @@ func TestLoadCapturesInheritedImportedContent(t *testing.T) {
if definition.Templates[0].Content != "Captured {{.name}}" || inspection.PromptHash == "" {
t.Fatalf("inherited file content was not captured: %+v, %+v", definition, inspection)
}
message := definition.Templates[0]
if message.Role != "user" || message.CacheControl == nil || message.CacheControl.Type != "ephemeral" || message.CacheControl.TTL != "1h" {
t.Fatalf("message metadata was not normalized: %+v", message)
}
field, ok := state.result.Metadata().Lookup(configkit.Keys("prompts", "child", "v1", "messages").Index(0).Key("content_file"))
if !ok || len(field.Origin.Locations) != 1 {
t.Fatalf("content origin was lost during normalization: %+v", field)
}
var originPath string
for _, source := range state.result.Sources() {
if source.ID == field.Origin.Locations[0].SourceID {
originPath = source.Path
}
}
if originPath != "catalog/base.yaml" {
t.Fatalf("content origin = %q, want catalog/base.yaml", originPath)
}
files["catalog/body.md"].Data = []byte("Changed")
again, againInspection, err := state.prompt("child", "v1")
if err != nil || again.Templates[0].Content != "Captured {{.name}}" || againInspection.PromptHash != inspection.PromptHash {