Separate prompt and target capture
This commit is contained in:
@@ -85,6 +85,8 @@ returning and retains no source capability for public inspection.
|
||||
|
||||
Prompt materialization shares a lazy metadata snapshot across root and collection
|
||||
resources during construction, avoiding a full metadata copy per collection.
|
||||
Input metadata, message content and templates, and output contracts are captured
|
||||
separately before the definition, inspection, and hash are assembled.
|
||||
|
||||
Normalization and defaults are recorded at individual resolved field paths,
|
||||
including message roles and cache controls. List indexes identify fields for
|
||||
@@ -146,8 +148,8 @@ change rendering behavior.
|
||||
ConfigKit resolves profile inheritance before the handoff. Within a collection,
|
||||
`extends: {id: parent}` names a sibling local ID; it is not a cross-collection reference.
|
||||
The configuration adapter validates final effective resource references and
|
||||
field values, then builds target
|
||||
snapshots with backend defaults and presence/suppression state. A profile can
|
||||
field values, then captures backend definitions and profile target snapshots
|
||||
with backend defaults and presence/suppression state. A profile can
|
||||
remain partial until runtime overrides complete it. `InspectProfile` selects
|
||||
that captured target without prompt, artifact, schema, credential lookup, or
|
||||
provider work. Preparation applies runtime overrides and then checks target
|
||||
|
||||
@@ -650,6 +650,11 @@ on Go 1.27.3; the extracted checks preserve their existing paths and rules.
|
||||
input metadata, inspection, and execution-selection tests keep their behavior.
|
||||
Update focused source/internal descriptions without duplicating format policy.
|
||||
|
||||
Completion: prompt inputs, messages, and output contracts are captured separately
|
||||
before final definition assembly, and backend/profile construction is isolated
|
||||
with presence handling intact. Configuration, prompt, engine, and focused public
|
||||
tests pass on Go 1.27.3; the internal source description is updated.
|
||||
|
||||
## Stage 25: Replace Parallel Attempt Returns With A Result Value
|
||||
|
||||
**Catalog:** 25, attempt boundary. **Files:** generation coordinator and tests.
|
||||
|
||||
@@ -72,103 +72,21 @@ func (state *loaded) materializePrompt(id string, raw promptConfig, base configk
|
||||
if raw.SessionID != nil {
|
||||
definition.SessionID = *raw.SessionID
|
||||
}
|
||||
seenInputs := make(map[string]bool, len(raw.Inputs))
|
||||
definition.Inputs = make([]domain.PromptInput, 0, len(raw.Inputs))
|
||||
entry := &promptEntry{
|
||||
messages: make([]*template.Template, 0, len(raw.Messages)),
|
||||
requirementInputs: make([]prompt.InputMetadata, 0, len(raw.Inputs)),
|
||||
inputs, inputMetadata, err := state.capturePromptInputs(id, raw.Inputs, base)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for index, input := range raw.Inputs {
|
||||
path := base.Key("inputs").Index(index)
|
||||
if input.Name == "" {
|
||||
return nil, fmt.Errorf("%s: input name is required", path.Key("name"))
|
||||
}
|
||||
if seenInputs[input.Name] {
|
||||
return nil, fmt.Errorf("%s: input name is duplicated", path.Key("name"))
|
||||
}
|
||||
seenInputs[input.Name] = true
|
||||
contentType := ""
|
||||
if input.ContentType != nil {
|
||||
contentType = *input.ContentType
|
||||
}
|
||||
description := ""
|
||||
if input.Description != nil {
|
||||
description = *input.Description
|
||||
}
|
||||
definition.Inputs = append(definition.Inputs, domain.PromptInput{
|
||||
Name: input.Name, Required: input.Required, ContentType: contentType, Description: description,
|
||||
})
|
||||
presence := state.promptMetadata[id][index]
|
||||
metadata := prompt.InputMetadata{Name: input.Name, Required: input.Required}
|
||||
if presence.contentType == fieldValue {
|
||||
metadata.ContentType = stringPointer(contentType)
|
||||
}
|
||||
if presence.description == fieldValue {
|
||||
metadata.Description = stringPointer(description)
|
||||
}
|
||||
entry.requirementInputs = append(entry.requirementInputs, metadata)
|
||||
messages, compiledMessages, err := state.capturePromptMessages(raw.Messages, base, lookupOrigin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
definition.Templates = make([]domain.PromptMessageTemplate, 0, len(raw.Messages))
|
||||
for index, message := range raw.Messages {
|
||||
path := base.Key("messages").Index(index)
|
||||
if message.Role == nil {
|
||||
return nil, fmt.Errorf("%s: role is required", path.Key("role"))
|
||||
}
|
||||
if _, err := domain.NormalizeMessageRole(*message.Role); 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)
|
||||
}
|
||||
content := ""
|
||||
if message.Content != nil {
|
||||
content = *message.Content
|
||||
} else {
|
||||
var err error
|
||||
content, err = state.readContent(path.Key("content_file"), *message.ContentFile, lookupOrigin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var control *domain.CacheControl
|
||||
if message.CacheControl != nil {
|
||||
if message.CacheControl.Type == nil {
|
||||
return nil, fmt.Errorf("%s: type is required", path.Key("cache_control").Key("type"))
|
||||
}
|
||||
control = &domain.CacheControl{Type: domain.CacheControlType(*message.CacheControl.Type)}
|
||||
if message.CacheControl.TTL != nil {
|
||||
control.TTL = *message.CacheControl.TTL
|
||||
}
|
||||
if _, err := domain.NormalizeCacheControl(control); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", path.Key("cache_control"), err)
|
||||
}
|
||||
}
|
||||
compiled, err := prompt.CompileTemplate(fmt.Sprintf("msg_%d", index), content)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: invalid template syntax: %w", path, err)
|
||||
}
|
||||
entry.messages = append(entry.messages, compiled)
|
||||
definition.Templates = append(definition.Templates, domain.PromptMessageTemplate{
|
||||
Role: *message.Role, Content: content, CacheControl: control,
|
||||
})
|
||||
}
|
||||
if raw.Output == nil {
|
||||
return nil, fmt.Errorf("%s: output is required", base.Key("output"))
|
||||
}
|
||||
if raw.Output.Format == nil || raw.Output.ValidationMode == nil {
|
||||
return nil, fmt.Errorf("%s: format and validation_mode are required", base.Key("output"))
|
||||
}
|
||||
contract := domain.OutputContract{
|
||||
Format: domain.OutputFormat(*raw.Output.Format),
|
||||
ValidationMode: domain.ValidationMode(*raw.Output.ValidationMode),
|
||||
RepairAttempts: raw.Output.RepairAttempts,
|
||||
}
|
||||
if raw.Output.SchemaPath != nil {
|
||||
contract.SchemaPath = *raw.Output.SchemaPath
|
||||
}
|
||||
if err := domain.ValidateOutputContract(contract); err != nil {
|
||||
return nil, fmt.Errorf("%s: invalid output contract: %w", base.Key("output"), err)
|
||||
contract, err := capturePromptOutput(raw.Output, base)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
definition.Inputs = inputs
|
||||
definition.Templates = messages
|
||||
entry := &promptEntry{requirementInputs: inputMetadata, messages: compiledMessages}
|
||||
definition.OutputFormat = contract.Format
|
||||
definition.Validation = contract
|
||||
if raw.SessionID != nil {
|
||||
@@ -192,6 +110,113 @@ func (state *loaded) materializePrompt(id string, raw promptConfig, base configk
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func (state *loaded) capturePromptInputs(id string, rawInputs []promptInputConfig, base configkit.Path) ([]domain.PromptInput, []prompt.InputMetadata, error) {
|
||||
seenInputs := make(map[string]bool, len(rawInputs))
|
||||
inputs := make([]domain.PromptInput, 0, len(rawInputs))
|
||||
metadata := make([]prompt.InputMetadata, 0, len(rawInputs))
|
||||
for index, input := range rawInputs {
|
||||
path := base.Key("inputs").Index(index)
|
||||
if input.Name == "" {
|
||||
return nil, nil, fmt.Errorf("%s: input name is required", path.Key("name"))
|
||||
}
|
||||
if seenInputs[input.Name] {
|
||||
return nil, nil, fmt.Errorf("%s: input name is duplicated", path.Key("name"))
|
||||
}
|
||||
seenInputs[input.Name] = true
|
||||
contentType := ""
|
||||
if input.ContentType != nil {
|
||||
contentType = *input.ContentType
|
||||
}
|
||||
description := ""
|
||||
if input.Description != nil {
|
||||
description = *input.Description
|
||||
}
|
||||
inputs = append(inputs, domain.PromptInput{
|
||||
Name: input.Name, Required: input.Required, ContentType: contentType, Description: description,
|
||||
})
|
||||
presence := state.promptMetadata[id][index]
|
||||
item := prompt.InputMetadata{Name: input.Name, Required: input.Required}
|
||||
if presence.contentType == fieldValue {
|
||||
item.ContentType = stringPointer(contentType)
|
||||
}
|
||||
if presence.description == fieldValue {
|
||||
item.Description = stringPointer(description)
|
||||
}
|
||||
metadata = append(metadata, item)
|
||||
}
|
||||
return inputs, metadata, nil
|
||||
}
|
||||
|
||||
func (state *loaded) capturePromptMessages(rawMessages []promptMessageConfig, base configkit.Path, lookupOrigin func(configkit.Path) (configkit.FieldInfo, bool)) ([]domain.PromptMessageTemplate, []*template.Template, error) {
|
||||
templates := make([]domain.PromptMessageTemplate, 0, len(rawMessages))
|
||||
compiledMessages := make([]*template.Template, 0, len(rawMessages))
|
||||
for index, message := range rawMessages {
|
||||
path := base.Key("messages").Index(index)
|
||||
if message.Role == nil {
|
||||
return nil, nil, fmt.Errorf("%s: role is required", path.Key("role"))
|
||||
}
|
||||
if _, err := domain.NormalizeMessageRole(*message.Role); err != nil {
|
||||
return nil, nil, fmt.Errorf("%s: %w", path.Key("role"), err)
|
||||
}
|
||||
if (message.Content != nil) == (message.ContentFile != nil) {
|
||||
return nil, nil, fmt.Errorf("%s: exactly one of content or content_file is required", path)
|
||||
}
|
||||
content := ""
|
||||
if message.Content != nil {
|
||||
content = *message.Content
|
||||
} else {
|
||||
var err error
|
||||
content, err = state.readContent(path.Key("content_file"), *message.ContentFile, lookupOrigin)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
var control *domain.CacheControl
|
||||
if message.CacheControl != nil {
|
||||
if message.CacheControl.Type == nil {
|
||||
return nil, nil, fmt.Errorf("%s: type is required", path.Key("cache_control").Key("type"))
|
||||
}
|
||||
control = &domain.CacheControl{Type: domain.CacheControlType(*message.CacheControl.Type)}
|
||||
if message.CacheControl.TTL != nil {
|
||||
control.TTL = *message.CacheControl.TTL
|
||||
}
|
||||
if _, err := domain.NormalizeCacheControl(control); err != nil {
|
||||
return nil, nil, fmt.Errorf("%s: %w", path.Key("cache_control"), err)
|
||||
}
|
||||
}
|
||||
compiled, err := prompt.CompileTemplate(fmt.Sprintf("msg_%d", index), content)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("%s: invalid template syntax: %w", path, err)
|
||||
}
|
||||
compiledMessages = append(compiledMessages, compiled)
|
||||
templates = append(templates, domain.PromptMessageTemplate{
|
||||
Role: *message.Role, Content: content, CacheControl: control,
|
||||
})
|
||||
}
|
||||
return templates, compiledMessages, nil
|
||||
}
|
||||
|
||||
func capturePromptOutput(output *promptOutputConfig, base configkit.Path) (domain.OutputContract, error) {
|
||||
if output == nil {
|
||||
return domain.OutputContract{}, fmt.Errorf("%s: output is required", base.Key("output"))
|
||||
}
|
||||
if output.Format == nil || output.ValidationMode == nil {
|
||||
return domain.OutputContract{}, fmt.Errorf("%s: format and validation_mode are required", base.Key("output"))
|
||||
}
|
||||
contract := domain.OutputContract{
|
||||
Format: domain.OutputFormat(*output.Format),
|
||||
ValidationMode: domain.ValidationMode(*output.ValidationMode),
|
||||
RepairAttempts: output.RepairAttempts,
|
||||
}
|
||||
if output.SchemaPath != nil {
|
||||
contract.SchemaPath = *output.SchemaPath
|
||||
}
|
||||
if err := domain.ValidateOutputContract(contract); err != nil {
|
||||
return domain.OutputContract{}, fmt.Errorf("%s: invalid output contract: %w", base.Key("output"), err)
|
||||
}
|
||||
return contract, nil
|
||||
}
|
||||
|
||||
func stringPointer(value string) *string {
|
||||
copy := value
|
||||
return ©
|
||||
|
||||
@@ -17,34 +17,7 @@ func (state *loaded) buildExecutionProfiles() error {
|
||||
definitions := make(map[string]domain.Backend, len(backendConfigs))
|
||||
for _, id := range sortedKeys(backendConfigs) {
|
||||
config := backendConfigs[id]
|
||||
definition := domain.Backend{ID: id, ExtraParams: config.ExtraParams}
|
||||
if config.SmokeTests != nil {
|
||||
smokeTests := &domain.BackendSmokeTestConfig{DefaultProfile: config.SmokeTests.DefaultProfile}
|
||||
if config.SmokeTests.Reachability != nil {
|
||||
smokeTests.Reachability = &domain.ReachabilityProbeConfig{Endpoint: config.SmokeTests.Reachability.Endpoint}
|
||||
}
|
||||
if config.SmokeTests.Authentication != nil {
|
||||
smokeTests.Authentication = &domain.AuthenticationProbeConfig{
|
||||
Kind: config.SmokeTests.Authentication.Kind,
|
||||
Endpoint: config.SmokeTests.Authentication.Endpoint,
|
||||
}
|
||||
}
|
||||
definition.SmokeTests = smokeTests
|
||||
}
|
||||
if config.Endpoint != nil {
|
||||
definition.Endpoint = *config.Endpoint
|
||||
}
|
||||
if config.APIKeyEnv != nil {
|
||||
definition.APIKeyEnv = *config.APIKeyEnv
|
||||
}
|
||||
if config.ConcurrencyLimit != nil {
|
||||
definition.ConcurrencyLimit = *config.ConcurrencyLimit
|
||||
}
|
||||
if config.QueueCapacity != nil {
|
||||
definition.QueueCapacity = *config.QueueCapacity
|
||||
definition.QueueCapacitySet = definition.ConcurrencyLimit > 0
|
||||
}
|
||||
definitions[id] = definition
|
||||
definitions[id] = captureBackend(id, config)
|
||||
}
|
||||
registry, err := backend.NewEffectiveRegistry(definitions)
|
||||
if err != nil {
|
||||
@@ -54,72 +27,110 @@ func (state *loaded) buildExecutionProfiles() error {
|
||||
state.indexedProfiles = make(map[string]domain.ExecutionProfile, len(profileConfigs))
|
||||
for _, id := range sortedKeys(profileConfigs) {
|
||||
config := profileConfigs[id]
|
||||
fields := state.profiles[id]
|
||||
target := defaults.ExecutionTargetDefault()
|
||||
if config.Backend != nil && *config.Backend != "" {
|
||||
selected, err := registry.GetBackend(*config.Backend)
|
||||
if err != nil {
|
||||
return fmt.Errorf("profile %q backend: %w", id, err)
|
||||
}
|
||||
target.BackendID = selected.ID
|
||||
target.Endpoint = selected.Endpoint
|
||||
target.APIKeyEnv = selected.APIKeyEnv
|
||||
target.ExtraParams = selected.ExtraParams
|
||||
profile, err := captureExecutionProfile(id, config, state.profiles[id], registry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
presence := domain.ExecutionTargetPresence{}
|
||||
if fields["endpoint"] != fieldAbsent {
|
||||
target.Endpoint = valueOrZero(config.Endpoint)
|
||||
}
|
||||
if fields["model"] != fieldAbsent {
|
||||
target.Model = valueOrZero(config.Model)
|
||||
}
|
||||
if fields["temperature"] != fieldAbsent {
|
||||
target.Temperature = valueOrZero(config.Temperature)
|
||||
presence.Temperature = fields["temperature"] == fieldValue
|
||||
}
|
||||
if fields["top_p"] != fieldAbsent {
|
||||
target.TopP = valueOrZero(config.TopP)
|
||||
presence.TopP = fields["top_p"] == fieldValue
|
||||
}
|
||||
if fields["max_tokens"] != fieldAbsent {
|
||||
target.MaxTokens = valueOrZero(config.MaxTokens)
|
||||
presence.MaxTokens = fields["max_tokens"] == fieldValue
|
||||
}
|
||||
if fields["timeout_seconds"] == fieldValue {
|
||||
target.TimeoutSeconds = valueOrZero(config.TimeoutSeconds)
|
||||
presence.TimeoutSeconds = true
|
||||
}
|
||||
if fields["service_tier"] != fieldAbsent {
|
||||
target.ServiceTier = valueOrZero(config.ServiceTier)
|
||||
}
|
||||
if fields["reasoning_effort"] != fieldAbsent {
|
||||
target.ReasoningEffort = valueOrZero(config.ReasoningEffort)
|
||||
}
|
||||
if fields["api_key_env"] != fieldAbsent {
|
||||
target.APIKeyEnv = valueOrZero(config.APIKeyEnv)
|
||||
}
|
||||
if fields["api_key_required"] != fieldAbsent {
|
||||
target.APIKeyRequired = valueOrZero(config.APIKeyRequired)
|
||||
}
|
||||
if target.APIKeyRequired && fields["api_key_env"] == fieldAbsent {
|
||||
target.APIKeyEnv = ""
|
||||
}
|
||||
switch fields["extra_params"] {
|
||||
case fieldNull:
|
||||
target.ExtraParams = nil
|
||||
case fieldValue:
|
||||
merged, err := overlayProviderMap(target.ExtraParams, config.ExtraParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("profile %q extra parameters: %w", id, err)
|
||||
}
|
||||
target.ExtraParams = merged
|
||||
}
|
||||
profile := domain.ExecutionProfile{ID: id, ResolvedTarget: &target, TargetPresence: presence}
|
||||
state.indexedProfiles[id] = profile
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func captureBackend(id string, config backendConfig) domain.Backend {
|
||||
definition := domain.Backend{ID: id, ExtraParams: config.ExtraParams}
|
||||
if config.SmokeTests != nil {
|
||||
smokeTests := &domain.BackendSmokeTestConfig{DefaultProfile: config.SmokeTests.DefaultProfile}
|
||||
if config.SmokeTests.Reachability != nil {
|
||||
smokeTests.Reachability = &domain.ReachabilityProbeConfig{Endpoint: config.SmokeTests.Reachability.Endpoint}
|
||||
}
|
||||
if config.SmokeTests.Authentication != nil {
|
||||
smokeTests.Authentication = &domain.AuthenticationProbeConfig{
|
||||
Kind: config.SmokeTests.Authentication.Kind,
|
||||
Endpoint: config.SmokeTests.Authentication.Endpoint,
|
||||
}
|
||||
}
|
||||
definition.SmokeTests = smokeTests
|
||||
}
|
||||
if config.Endpoint != nil {
|
||||
definition.Endpoint = *config.Endpoint
|
||||
}
|
||||
if config.APIKeyEnv != nil {
|
||||
definition.APIKeyEnv = *config.APIKeyEnv
|
||||
}
|
||||
if config.ConcurrencyLimit != nil {
|
||||
definition.ConcurrencyLimit = *config.ConcurrencyLimit
|
||||
}
|
||||
if config.QueueCapacity != nil {
|
||||
definition.QueueCapacity = *config.QueueCapacity
|
||||
definition.QueueCapacitySet = definition.ConcurrencyLimit > 0
|
||||
}
|
||||
return definition
|
||||
}
|
||||
|
||||
func captureExecutionProfile(id string, config profileConfig, fields map[string]fieldState, registry *backend.Registry) (domain.ExecutionProfile, error) {
|
||||
target := defaults.ExecutionTargetDefault()
|
||||
if config.Backend != nil && *config.Backend != "" {
|
||||
selected, err := registry.GetBackend(*config.Backend)
|
||||
if err != nil {
|
||||
return domain.ExecutionProfile{}, fmt.Errorf("profile %q backend: %w", id, err)
|
||||
}
|
||||
target.BackendID = selected.ID
|
||||
target.Endpoint = selected.Endpoint
|
||||
target.APIKeyEnv = selected.APIKeyEnv
|
||||
target.ExtraParams = selected.ExtraParams
|
||||
}
|
||||
presence := domain.ExecutionTargetPresence{}
|
||||
if fields["endpoint"] != fieldAbsent {
|
||||
target.Endpoint = valueOrZero(config.Endpoint)
|
||||
}
|
||||
if fields["model"] != fieldAbsent {
|
||||
target.Model = valueOrZero(config.Model)
|
||||
}
|
||||
if fields["temperature"] != fieldAbsent {
|
||||
target.Temperature = valueOrZero(config.Temperature)
|
||||
presence.Temperature = fields["temperature"] == fieldValue
|
||||
}
|
||||
if fields["top_p"] != fieldAbsent {
|
||||
target.TopP = valueOrZero(config.TopP)
|
||||
presence.TopP = fields["top_p"] == fieldValue
|
||||
}
|
||||
if fields["max_tokens"] != fieldAbsent {
|
||||
target.MaxTokens = valueOrZero(config.MaxTokens)
|
||||
presence.MaxTokens = fields["max_tokens"] == fieldValue
|
||||
}
|
||||
if fields["timeout_seconds"] == fieldValue {
|
||||
target.TimeoutSeconds = valueOrZero(config.TimeoutSeconds)
|
||||
presence.TimeoutSeconds = true
|
||||
}
|
||||
if fields["service_tier"] != fieldAbsent {
|
||||
target.ServiceTier = valueOrZero(config.ServiceTier)
|
||||
}
|
||||
if fields["reasoning_effort"] != fieldAbsent {
|
||||
target.ReasoningEffort = valueOrZero(config.ReasoningEffort)
|
||||
}
|
||||
if fields["api_key_env"] != fieldAbsent {
|
||||
target.APIKeyEnv = valueOrZero(config.APIKeyEnv)
|
||||
}
|
||||
if fields["api_key_required"] != fieldAbsent {
|
||||
target.APIKeyRequired = valueOrZero(config.APIKeyRequired)
|
||||
}
|
||||
if target.APIKeyRequired && fields["api_key_env"] == fieldAbsent {
|
||||
target.APIKeyEnv = ""
|
||||
}
|
||||
switch fields["extra_params"] {
|
||||
case fieldNull:
|
||||
target.ExtraParams = nil
|
||||
case fieldValue:
|
||||
merged, err := overlayProviderMap(target.ExtraParams, config.ExtraParams)
|
||||
if err != nil {
|
||||
return domain.ExecutionProfile{}, fmt.Errorf("profile %q extra parameters: %w", id, err)
|
||||
}
|
||||
target.ExtraParams = merged
|
||||
}
|
||||
profile := domain.ExecutionProfile{ID: id, ResolvedTarget: &target, TargetPresence: presence}
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
func valueOrZero[T any](value *T) T {
|
||||
if value == nil {
|
||||
var zero T
|
||||
|
||||
Reference in New Issue
Block a user