218 lines
9.0 KiB
Go
218 lines
9.0 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"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contractfinal"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var marshalAgentSelectionFixture = json.Marshal
|
|
|
|
const agentSelectionFixtureVersion = 1
|
|
|
|
// AgentSelectionCase is one reproducible model-evaluation assertion derived
|
|
// from ContractFinal Selection prose. 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 AgentSelectionCase 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"`
|
|
}
|
|
|
|
// AgentSelectionFixture is the stable input to an optional live Agent
|
|
// evaluation. It is built from contract.ProductDecl/ContractFinal selection prose and
|
|
// the real bound command tree; it is not a second authored hint source.
|
|
type AgentSelectionFixture struct {
|
|
Version int `json:"version"`
|
|
Cases []AgentSelectionCase `json:"cases"`
|
|
}
|
|
|
|
// AgentSelectionReport 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 AgentSelectionReport struct {
|
|
Tools int
|
|
PositiveAssertions int
|
|
NegativeAssertions int
|
|
FixtureSHA256 string
|
|
}
|
|
|
|
// BuildAgentSelectionEvalFixture turns every ContractFinal use_when and
|
|
// avoid_when entry into a typed, reproducible evaluation case and validates it
|
|
// against the exact BoundCommandRegistry.
|
|
func BuildAgentSelectionEvalFixture(bound BoundCommandRegistry) (AgentSelectionFixture, AgentSelectionReport, error) {
|
|
fixture := AgentSelectionFixture{Version: agentSelectionFixtureVersion}
|
|
report := AgentSelectionReport{}
|
|
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
|
|
if !contractfinal.HasRuntimeContractFinal(command.PrimaryCommand) {
|
|
return fixture, report, fmt.Errorf("agent selection expected canonical %q has no ContractFinal declaration", canonical)
|
|
}
|
|
productID, _, ok := strings.Cut(canonical, ".")
|
|
if !ok || strings.TrimSpace(productID) == "" {
|
|
return fixture, report, fmt.Errorf("agent selection has invalid bound canonical path %q", canonical)
|
|
}
|
|
candidatesByProduct[productID] = append(candidatesByProduct[productID], canonical)
|
|
}
|
|
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 selection expected canonical %q is missing from BoundCommandRegistry.ByCanonical", canonical)
|
|
}
|
|
if err := validateAgentSelectionBinding(bound, canonical, command); err != nil {
|
|
return fixture, report, err
|
|
}
|
|
|
|
selection := contractFinalToolSelection(command.PrimaryCommand)
|
|
if len(selection.UseWhen) == 0 {
|
|
return fixture, report, fmt.Errorf("ContractFinal tool %s requires at least one positive use_when selection assertion", canonical)
|
|
}
|
|
if len(selection.AvoidWhen) == 0 {
|
|
return fixture, report, fmt.Errorf("ContractFinal tool %s requires at least one negative avoid_when selection assertion", canonical)
|
|
}
|
|
productID, _, _ := strings.Cut(canonical, ".")
|
|
candidates := candidatesByProduct[productID]
|
|
for index, scenario := range selection.UseWhen {
|
|
normalized := normalizeAgentSelectionScenario(scenario)
|
|
if normalized == "" {
|
|
return fixture, report, fmt.Errorf("ContractFinal tool %s has an empty normalized use_when selection assertion", canonical)
|
|
}
|
|
if previous, exists := positiveExpectations[normalized]; exists {
|
|
return fixture, report, fmt.Errorf("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, AgentSelectionCase{
|
|
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 selection.AvoidWhen {
|
|
normalized := normalizeAgentSelectionScenario(scenario)
|
|
if normalized == "" {
|
|
return fixture, report, fmt.Errorf("ContractFinal tool %s has an empty normalized avoid_when selection assertion", canonical)
|
|
}
|
|
if expected := positiveExpectations[normalized]; expected == canonical {
|
|
return fixture, report, fmt.Errorf("ContractFinal tool %s has the same literal positive and negative selection scenario %q", canonical, strings.TrimSpace(scenario))
|
|
}
|
|
fixture.Cases = append(fixture.Cases, AgentSelectionCase{
|
|
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 := agentSelectionFixtureDigest(fixture)
|
|
if err != nil {
|
|
return fixture, report, err
|
|
}
|
|
report.FixtureSHA256 = digest
|
|
return fixture, report, nil
|
|
}
|
|
|
|
// ValidateAgentSelectionContract is the lightweight generator-facing gate.
|
|
func ValidateAgentSelectionContract(bound BoundCommandRegistry) (AgentSelectionReport, error) {
|
|
_, report, err := BuildAgentSelectionEvalFixture(bound)
|
|
return report, err
|
|
}
|
|
|
|
// contractFinalToolSelection synthesizes selection assertions of a declared
|
|
// tool from its ContractFinal declaration.
|
|
func contractFinalToolSelection(command *cobra.Command) AgentToolSelection {
|
|
out := AgentToolSelection{Reviewed: true, Revision: "contract", Reason: "Contract final declaration (corecmd.ContractDecl)"}
|
|
payload, ok := contractfinal.RuntimeContractFinal(command)
|
|
if !ok || payload.Selection == nil {
|
|
return out
|
|
}
|
|
selection := payload.Selection
|
|
out.AgentSummary = selection.AgentSummary
|
|
out.UseWhen = selection.UseWhen
|
|
out.AvoidWhen = selection.AvoidWhen
|
|
out.Examples = selection.Examples
|
|
out.ExampleDispositions = selection.ExampleDispositions
|
|
return out
|
|
}
|
|
|
|
func validateAgentSelectionBinding(bound BoundCommandRegistry, canonical string, command BoundCommandSpec) error {
|
|
if command.CanonicalPath != canonical {
|
|
return fmt.Errorf("agent selection canonical %q resolves to mismatched BoundCommandRegistry entry %q", canonical, command.CanonicalPath)
|
|
}
|
|
if command.PrimaryCommand == nil {
|
|
return fmt.Errorf("agent selection canonical %q has no bound primary Cobra command", canonical)
|
|
}
|
|
if !runnableSchemaLeaf(command.PrimaryCommand) {
|
|
return fmt.Errorf("agent 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 selection canonical %q has an empty primary CLI path", canonical)
|
|
}
|
|
byPath, ok := bound.ByCLIPath[primaryPath]
|
|
if !ok || byPath.CanonicalPath != canonical {
|
|
return fmt.Errorf("agent selection canonical %q primary path %q is not bound back to the same tool", canonical, primaryPath)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func normalizeAgentSelectionScenario(value string) string {
|
|
return strings.ToLower(strings.Join(strings.Fields(strings.TrimSpace(value)), " "))
|
|
}
|
|
|
|
func agentSelectionFixtureDigest(fixture AgentSelectionFixture) (string, error) {
|
|
data, err := marshalAgentSelectionFixture(fixture)
|
|
if err != nil {
|
|
return "", fmt.Errorf("marshal Agent selection fixture: %w", err)
|
|
}
|
|
digest := sha256.Sum256(data)
|
|
return "sha256:" + hex.EncodeToString(digest[:]), nil
|
|
}
|