Clarify semantic reconciliation candidate numbers

This commit is contained in:
2026-08-29 03:16:24 +00:00
parent 917d150279
commit 7e626753bf
30 changed files with 572 additions and 196 deletions

View File

@@ -70,18 +70,26 @@ const (
LimitExceeded
)
// CandidateMapping relates one model-visible request-local ID to the
// CandidateMapping relates one model-visible request-local number to the
// corresponding zero-based position in the caller's candidate slice.
type CandidateMapping struct {
CandidateID int
CandidateNumber int
CandidatePosition int
}
// CandidateNumberRange is the exact inclusive request-local number range
// presented to the model. Ready preparations always use a one-based range.
type CandidateNumberRange struct {
First int `json:"first"`
Last int `json:"last"`
}
// Preparation owns the visible candidate mapping and prompt materials.
type Preparation struct {
disposition Disposition
mappings []CandidateMapping
materials contracts.LLMInputSet
disposition Disposition
candidateNumberRange CandidateNumberRange
mappings []CandidateMapping
materials contracts.LLMInputSet
}
// Disposition returns the preparation outcome.
@@ -94,6 +102,12 @@ func (preparation Preparation) CandidateMappings() []CandidateMapping {
return append([]CandidateMapping(nil), preparation.mappings...)
}
// CandidateNumberRange returns the exact request-local range assigned during
// preparation. A zero value means that no eligible candidates were present.
func (preparation Preparation) CandidateNumberRange() CandidateNumberRange {
return preparation.candidateNumberRange
}
// Materials returns independently owned candidate and transcript materials.
// It is empty unless Disposition returns Ready.
func (preparation Preparation) Materials() contracts.LLMInputSet {
@@ -106,13 +120,14 @@ type sourceRange struct {
}
type visibleCandidate struct {
CandidateID int `json:"candidate_id"`
Label string `json:"label"`
SourceRefs []sourceRange `json:"source_refs"`
CandidateNumber int `json:"candidate_number"`
Label string `json:"label"`
SourceRefs []sourceRange `json:"source_refs"`
}
type candidateInput struct {
Candidates []visibleCandidate `json:"candidates"`
CandidateNumberRange CandidateNumberRange `json:"candidate_number_range"`
Candidates []visibleCandidate `json:"candidates"`
}
type transcriptInput struct {
@@ -168,17 +183,20 @@ func Prepare(document *source.SourceDocument, candidates []Candidate, limits Lim
disposition: InsufficientCandidates,
mappings: make([]CandidateMapping, len(prepared)),
}
if len(prepared) > 0 {
result.candidateNumberRange = CandidateNumberRange{First: 1, Last: len(prepared)}
}
views := make([]visibleCandidate, len(prepared))
for index, candidate := range prepared {
candidateID := index + 1
candidateNumber := index + 1
result.mappings[index] = CandidateMapping{
CandidateID: candidateID,
CandidateNumber: candidateNumber,
CandidatePosition: candidate.position,
}
views[index] = visibleCandidate{
CandidateID: candidateID,
Label: candidates[candidate.position].Label,
SourceRefs: cloneSourceRanges(candidate.references),
CandidateNumber: candidateNumber,
Label: candidates[candidate.position].Label,
SourceRefs: cloneSourceRanges(candidate.references),
}
}
if len(prepared) < 2 {
@@ -189,7 +207,7 @@ func Prepare(document *source.SourceDocument, candidates []Candidate, limits Lim
return result, nil
}
candidateContent, withinLimit, err := marshalCandidateInput(views, limits.MaximumMaterialBytes)
candidateContent, withinLimit, err := marshalCandidateInput(result.candidateNumberRange, views, limits.MaximumMaterialBytes)
if err != nil {
return Preparation{}, fmt.Errorf("prepare semantic reconciliation: encode candidate material: %w", err)
}
@@ -302,10 +320,14 @@ func coalesceIntervals(intervals []sourceInterval) []sourceInterval {
return coalesced
}
func marshalCandidateInput(candidates []visibleCandidate, maximumBytes int) ([]byte, bool, error) {
func marshalCandidateInput(numberRange CandidateNumberRange, candidates []visibleCandidate, maximumBytes int) ([]byte, bool, error) {
content := make([]byte, 0, min(maximumBytes, 4096))
encodedRange, err := json.Marshal(numberRange)
if err != nil {
return nil, false, err
}
var withinLimit bool
content, withinLimit = appendWithinLimit(content, maximumBytes, []byte(`{"candidates":[`))
content, withinLimit = appendWithinLimit(content, maximumBytes, []byte(`{"candidate_number_range":`), encodedRange, []byte(`,"candidates":[`))
if !withinLimit {
return nil, false, nil
}