205 lines
8.8 KiB
Go
205 lines
8.8 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package cli
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
var marshalManualAgentSelectionFixture = json.Marshal
|
|
|
|
const manualAgentSelectionFixtureVersion = 1
|
|
|
|
// ManualAgentSelectionCase is one reproducible model-evaluation assertion
|
|
// derived from reviewed Manual Agent hints. Positive cases require one exact
|
|
// canonical result; negative cases only forbid the command that owns the
|
|
// avoid_when text. CandidateCanonicals contains every bound tool in the same
|
|
// product, in stable order.
|
|
type ManualAgentSelectionCase struct {
|
|
ID string `json:"id"`
|
|
ProductID string `json:"product_id"`
|
|
Scenario string `json:"scenario"`
|
|
ExpectedCanonical string `json:"expected_canonical,omitempty"`
|
|
ForbiddenCanonical string `json:"forbidden_canonical,omitempty"`
|
|
CandidateCanonicals []string `json:"candidate_canonicals"`
|
|
}
|
|
|
|
// ManualAgentSelectionFixture is the stable input to an optional live Agent
|
|
// evaluation. It is built from schema_hints/selection and the real bound
|
|
// command tree; it is not a second authored hint source.
|
|
type ManualAgentSelectionFixture struct {
|
|
Version int `json:"version"`
|
|
Cases []ManualAgentSelectionCase `json:"cases"`
|
|
}
|
|
|
|
// ManualAgentSelectionReport records deterministic coverage and the exact
|
|
// fixture digest. It proves that all reviewed assertions are well-formed and
|
|
// executable; it deliberately does not claim that a language model understood
|
|
// their natural-language meaning.
|
|
type ManualAgentSelectionReport struct {
|
|
Tools int
|
|
PositiveAssertions int
|
|
NegativeAssertions int
|
|
FixtureSHA256 string
|
|
}
|
|
|
|
// BuildManualAgentSelectionEvalFixture turns every use_when and avoid_when
|
|
// entry into a typed, reproducible evaluation case and validates it against
|
|
// the exact BoundCommandRegistry.
|
|
//
|
|
// Deterministic CI can prove full coverage, real command binding, and absence
|
|
// of literal contradictory expectations. Semantic command choice belongs to
|
|
// an opt-in live-model evaluation that consumes this fixture; this function
|
|
// never substitutes string matching for Agent behavior.
|
|
func BuildManualAgentSelectionEvalFixture(bound BoundCommandRegistry, hints ManualAgentHintSet) (ManualAgentSelectionFixture, ManualAgentSelectionReport, error) {
|
|
fixture := ManualAgentSelectionFixture{Version: manualAgentSelectionFixtureVersion}
|
|
report := ManualAgentSelectionReport{}
|
|
expectedTools := make(map[string]bool, len(bound.Commands))
|
|
candidatesByProduct := map[string][]string{}
|
|
for _, command := range bound.Commands {
|
|
canonical := strings.TrimSpace(command.CanonicalPath)
|
|
expectedTools[canonical] = true
|
|
productID, _, ok := strings.Cut(canonical, ".")
|
|
if !ok || strings.TrimSpace(productID) == "" {
|
|
return fixture, report, fmt.Errorf("agent_hints selection has invalid bound canonical path %q", canonical)
|
|
}
|
|
candidatesByProduct[productID] = append(candidatesByProduct[productID], canonical)
|
|
}
|
|
if err := validateManualAgentHintExactSet("selection tools", expectedTools, mapKeysManualAgentTools(hints.Tools)); err != nil {
|
|
return fixture, report, err
|
|
}
|
|
for productID := range candidatesByProduct {
|
|
sort.Strings(candidatesByProduct[productID])
|
|
}
|
|
|
|
canonicals := make([]string, 0, len(expectedTools))
|
|
for canonical := range expectedTools {
|
|
canonicals = append(canonicals, canonical)
|
|
}
|
|
sort.Strings(canonicals)
|
|
|
|
positiveExpectations := map[string]string{}
|
|
positiveDisplays := map[string]string{}
|
|
for _, canonical := range canonicals {
|
|
command, ok := bound.ByCanonical[canonical]
|
|
if !ok {
|
|
return fixture, report, fmt.Errorf("agent_hints selection expected canonical %q is missing from BoundCommandRegistry.ByCanonical", canonical)
|
|
}
|
|
if err := validateManualAgentSelectionBinding(bound, canonical, command); err != nil {
|
|
return fixture, report, err
|
|
}
|
|
|
|
hint := hints.Tools[canonical]
|
|
if len(hint.UseWhen) == 0 {
|
|
return fixture, report, fmt.Errorf("agent_hints tool %s requires at least one positive use_when selection assertion", canonical)
|
|
}
|
|
if len(hint.AvoidWhen) == 0 {
|
|
return fixture, report, fmt.Errorf("agent_hints tool %s requires at least one negative avoid_when selection assertion", canonical)
|
|
}
|
|
productID, _, _ := strings.Cut(canonical, ".")
|
|
candidates := candidatesByProduct[productID]
|
|
for index, scenario := range hint.UseWhen {
|
|
normalized := normalizeManualAgentSelectionScenario(scenario)
|
|
if normalized == "" {
|
|
return fixture, report, fmt.Errorf("agent_hints tool %s has an empty normalized use_when selection assertion", canonical)
|
|
}
|
|
if previous, exists := positiveExpectations[normalized]; exists {
|
|
return fixture, report, fmt.Errorf("agent_hints use_when scenario %q has conflicting literal expectations %q and %q", positiveDisplays[normalized], previous, canonical)
|
|
}
|
|
positiveExpectations[normalized] = canonical
|
|
positiveDisplays[normalized] = strings.TrimSpace(scenario)
|
|
fixture.Cases = append(fixture.Cases, ManualAgentSelectionCase{
|
|
ID: fmt.Sprintf("%s/use_when/%d", canonical, index),
|
|
ProductID: productID,
|
|
Scenario: strings.TrimSpace(scenario),
|
|
ExpectedCanonical: canonical,
|
|
CandidateCanonicals: append([]string(nil), candidates...),
|
|
})
|
|
report.PositiveAssertions++
|
|
}
|
|
for index, scenario := range hint.AvoidWhen {
|
|
normalized := normalizeManualAgentSelectionScenario(scenario)
|
|
if normalized == "" {
|
|
return fixture, report, fmt.Errorf("agent_hints tool %s has an empty normalized avoid_when selection assertion", canonical)
|
|
}
|
|
if expected := positiveExpectations[normalized]; expected == canonical {
|
|
return fixture, report, fmt.Errorf("agent_hints tool %s has the same literal positive and negative selection scenario %q", canonical, strings.TrimSpace(scenario))
|
|
}
|
|
fixture.Cases = append(fixture.Cases, ManualAgentSelectionCase{
|
|
ID: fmt.Sprintf("%s/avoid_when/%d", canonical, index),
|
|
ProductID: productID,
|
|
Scenario: strings.TrimSpace(scenario),
|
|
ForbiddenCanonical: canonical,
|
|
CandidateCanonicals: append([]string(nil), candidates...),
|
|
})
|
|
report.NegativeAssertions++
|
|
}
|
|
}
|
|
|
|
report.Tools = len(canonicals)
|
|
digest, err := manualAgentSelectionFixtureDigest(fixture)
|
|
if err != nil {
|
|
return fixture, report, err
|
|
}
|
|
report.FixtureSHA256 = digest
|
|
return fixture, report, nil
|
|
}
|
|
|
|
// ValidateManualAgentSelectionContract is the lightweight generator-facing
|
|
// gate. Callers that run a semantic model evaluation should use
|
|
// BuildManualAgentSelectionEvalFixture and pass the returned cases to it.
|
|
func ValidateManualAgentSelectionContract(bound BoundCommandRegistry, hints ManualAgentHintSet) (ManualAgentSelectionReport, error) {
|
|
_, report, err := BuildManualAgentSelectionEvalFixture(bound, hints)
|
|
return report, err
|
|
}
|
|
|
|
func validateManualAgentSelectionBinding(bound BoundCommandRegistry, canonical string, command BoundCommandSpec) error {
|
|
if command.CanonicalPath != canonical {
|
|
return fmt.Errorf("agent_hints selection canonical %q resolves to mismatched BoundCommandRegistry entry %q", canonical, command.CanonicalPath)
|
|
}
|
|
if command.PrimaryCommand == nil {
|
|
return fmt.Errorf("agent_hints selection canonical %q has no bound primary Cobra command", canonical)
|
|
}
|
|
if !runnableSchemaLeaf(command.PrimaryCommand) {
|
|
return fmt.Errorf("agent_hints selection canonical %q primary path %q is not a runnable Cobra leaf", canonical, command.PrimaryCLIPath)
|
|
}
|
|
primaryPath := normalizeSchemaCLIPath(command.PrimaryCLIPath)
|
|
if primaryPath == "" {
|
|
return fmt.Errorf("agent_hints selection canonical %q has an empty primary CLI path", canonical)
|
|
}
|
|
byPath, ok := bound.ByCLIPath[primaryPath]
|
|
if !ok || byPath.CanonicalPath != canonical {
|
|
return fmt.Errorf("agent_hints selection canonical %q primary path %q is not bound back to the same tool", canonical, primaryPath)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func normalizeManualAgentSelectionScenario(value string) string {
|
|
return strings.ToLower(strings.Join(strings.Fields(strings.TrimSpace(value)), " "))
|
|
}
|
|
|
|
func manualAgentSelectionFixtureDigest(fixture ManualAgentSelectionFixture) (string, error) {
|
|
data, err := marshalManualAgentSelectionFixture(fixture)
|
|
if err != nil {
|
|
return "", fmt.Errorf("marshal Manual Agent selection fixture: %w", err)
|
|
}
|
|
digest := sha256.Sum256(data)
|
|
return "sha256:" + hex.EncodeToString(digest[:]), nil
|
|
}
|