Store one prompt definition per ID

This commit is contained in:
2026-10-17 05:41:21 +00:00
parent 0cd833513c
commit d40a010538
28 changed files with 393 additions and 445 deletions

View File

@@ -16,8 +16,7 @@ import (
)
var (
errPromptNotFound = errors.New("prompt definition not found")
errPromptAmbiguous = errors.New("prompt version is ambiguous")
errPromptNotFound = errors.New("prompt definition not found")
)
type promptEntry struct {
@@ -29,64 +28,61 @@ type promptEntry struct {
messages []*template.Template
}
func resolvePrompts(prompts map[string]map[string]promptConfig, basePath configkit.Path, resolution *configkit.Resolution) {
func resolvePrompts(prompts map[string]promptConfig, basePath configkit.Path, resolution *configkit.Resolution) {
for _, id := range sortedKeys(prompts) {
for _, version := range sortedKeys(prompts[id]) {
definition := prompts[id][version]
base := basePath.Key(id).Key(version)
definition := prompts[id]
base := basePath.Key(id)
for _, field := range []struct {
name string
value **string
}{
{"description", &definition.Description},
{"session_id", &definition.SessionID},
} {
if *field.value != nil {
normalizeString(field.value, base.Key(field.name), resolution, strings.TrimSpace(**field.value))
}
}
inputsChanged := false
for index := range definition.Inputs {
input := &definition.Inputs[index]
inputPath := base.Key("inputs").Index(index)
normalizedName := strings.TrimSpace(input.Name)
if normalizedName != input.Name {
configkit.Normalize(resolution, inputPath.Key("name"), &input.Name, normalizedName, "normalize input metadata")
inputsChanged = true
}
for _, field := range []struct {
name string
value **string
}{
{"description", &definition.Description},
{"session_id", &definition.SessionID},
{"content_type", &input.ContentType},
{"description", &input.Description},
} {
if *field.value != nil {
normalizeString(field.value, base.Key(field.name), resolution, strings.TrimSpace(**field.value))
}
}
inputsChanged := false
for index := range definition.Inputs {
input := &definition.Inputs[index]
inputPath := base.Key("inputs").Index(index)
normalizedName := strings.TrimSpace(input.Name)
if normalizedName != input.Name {
configkit.Normalize(resolution, inputPath.Key("name"), &input.Name, normalizedName, "normalize input metadata")
inputsChanged = true
}
for _, field := range []struct {
name string
value **string
}{
{"content_type", &input.ContentType},
{"description", &input.Description},
} {
if *field.value != nil {
normalized := strings.TrimSpace(**field.value)
if normalized != **field.value {
configkit.Normalize(resolution, inputPath.Key(field.name), field.value, &normalized, "normalize input metadata")
inputsChanged = true
}
normalized := strings.TrimSpace(**field.value)
if normalized != **field.value {
configkit.Normalize(resolution, inputPath.Key(field.name), field.value, &normalized, "normalize input metadata")
inputsChanged = true
}
}
}
if inputsChanged {
resolution.Record(base.Key("inputs"), configkit.ChangeNormalization, "normalize input 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))
}
prompts[id][version] = definition
}
if inputsChanged {
resolution.Record(base.Key("inputs"), configkit.ChangeNormalization, "normalize input 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))
}
prompts[id] = definition
}
}
func (state *loaded) materializePrompts(prompts map[string]map[string]promptConfig, basePath configkit.Path) error {
state.prompts = make(map[string]map[string]*promptEntry, len(prompts))
state.ambiguous = make(map[string]bool)
func (state *loaded) materializePrompts(prompts map[string]promptConfig, basePath configkit.Path) error {
state.prompts = make(map[string]*promptEntry, len(prompts))
var metadata *configkit.Metadata
lookupOrigin := func(path configkit.Path) (configkit.FieldInfo, bool) {
if metadata == nil {
@@ -100,23 +96,15 @@ func (state *loaded) materializePrompts(prompts map[string]map[string]promptConf
if strings.TrimSpace(id) == "" {
return fmt.Errorf("%s: prompt ID must not be blank", promptBase)
}
versions := prompts[id]
if versions == nil {
return fmt.Errorf("%s: prompt collection must be a mapping", promptBase)
version := ""
if prompts[id].Version != nil {
version = *prompts[id].Version
}
state.prompts[id] = make(map[string]*promptEntry, len(versions))
state.ambiguous[id] = len(versions) > 1
for _, version := range sortedKeys(versions) {
versionBase := promptBase.Key(version)
if strings.TrimSpace(version) == "" {
return fmt.Errorf("%s: prompt version must not be blank", versionBase)
}
entry, err := state.materializePrompt(id, version, versions[version], versionBase, lookupOrigin)
if err != nil {
return err
}
state.prompts[id][version] = entry
entry, err := state.materializePrompt(id, version, prompts[id], promptBase, lookupOrigin)
if err != nil {
return err
}
state.prompts[id] = entry
}
return nil
}
@@ -164,7 +152,7 @@ func (state *loaded) materializePrompt(id, version string, raw promptConfig, bas
definition.Inputs = append(definition.Inputs, domain.PromptInput{
Name: input.Name, Required: input.Required, ContentType: contentType, Description: description,
})
presence := state.promptMetadata[id][version][index]
presence := state.promptMetadata[id][index]
metadata := prompt.InputMetadata{Name: input.Name, Required: input.Required}
if presence.contentType == fieldValue {
metadata.ContentType = stringPointer(contentType)
@@ -299,19 +287,10 @@ func (state *loaded) readContent(fieldPath configkit.Path, name string, lookupOr
}
func (state *loaded) lookupPromptEntry(id, version string) (*promptEntry, error) {
if state == nil || strings.TrimSpace(id) == "" || (version != "" && strings.TrimSpace(version) == "") {
if state == nil || strings.TrimSpace(id) == "" {
return nil, errPromptNotFound
}
versions := state.prompts[id]
if version == "" {
if state.ambiguous[id] {
return nil, errPromptAmbiguous
}
for selected := range versions {
version = selected
}
}
entry := versions[version]
entry := state.prompts[id]
if entry == nil {
return nil, errPromptNotFound
}
@@ -342,8 +321,8 @@ func promptLookupError(err error) error {
return err
}
func (state *loaded) prompt(id, version string) (*domain.PromptDefinition, domain.PromptInspection, error) {
entry, err := state.lookupPromptEntry(id, version)
func (state *loaded) prompt(id string) (*domain.PromptDefinition, domain.PromptInspection, error) {
entry, err := state.lookupPromptEntry(id, "")
if err != nil {
return nil, domain.PromptInspection{}, err
}
@@ -351,7 +330,8 @@ func (state *loaded) prompt(id, version string) (*domain.PromptDefinition, domai
}
// GetPromptSnapshot returns an independently owned captured definition and its
// construction-time hash. Lookup retains literal ID and version keys.
// construction-time hash. The version argument is retained temporarily at the
// adapter boundary and does not participate in lookup.
func (state *loaded) GetPromptSnapshot(ctx context.Context, id, version string) (*domain.PromptDefinition, string, error) {
if err := ctx.Err(); err != nil {
return nil, "", err
@@ -382,12 +362,12 @@ func (state *loaded) GetPromptDefinition(ctx context.Context, id, version string
return definition, err
}
// GetCompiledTemplates returns construction-time parses for one exact prompt.
// GetCompiledTemplates returns construction-time parses for one prompt ID.
func (state *loaded) GetCompiledTemplates(id, version string) (prompt.CompiledTemplates, bool) {
if state == nil {
return prompt.CompiledTemplates{}, false
}
entry := state.prompts[id][version]
entry := state.prompts[id]
if entry == nil {
return prompt.CompiledTemplates{}, false
}
@@ -395,13 +375,13 @@ func (state *loaded) GetCompiledTemplates(id, version string) (prompt.CompiledTe
}
// GetRequirementSnapshot returns captured templates and input metadata for
// one exact prompt identity. Returned metadata and slices are independently
// one prompt ID. Returned metadata and slices are independently
// owned; the compiled templates are shared immutable construction-time parses.
func (state *loaded) GetRequirementSnapshot(id, version string) (prompt.RequirementSnapshot, bool) {
if state == nil {
return prompt.RequirementSnapshot{}, false
}
entry := state.prompts[id][version]
entry := state.prompts[id]
if entry == nil {
return prompt.RequirementSnapshot{}, false
}