Files
go-sip/internal/ai/snapshot.go
T

115 lines
3.0 KiB
Go

// Package ai handles immutable Agent-version snapshots. The JSON Schema is
// loaded from the pinned upstream contract bundle; this package does not
// duplicate or loosen that schema.
package ai
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"sync"
"git.ipao.vip/rogee/go-sip/contracts"
"git.ipao.vip/rogee/go-sip/internal/contract"
)
type Mode string
const (
ModeFullAI Mode = "full_ai"
ModeASROnly Mode = "asr_only"
)
type Snapshot struct {
TenantKey string
AgentVersionID string
Digest string
Raw []byte
Mode Mode
}
func Validate(raw []byte) (Snapshot, error) {
if err := contract.ValidateSourceSchema("ai-config.schema.json", raw); err != nil {
return Snapshot{}, err
}
var value struct {
AgentVersionID string `json:"agent_version_id"`
Mode Mode `json:"mode"`
}
if err := json.Unmarshal(raw, &value); err != nil {
return Snapshot{}, err
}
mode := value.Mode
if mode == "" {
// Legacy immutable snapshots predate the explicit mode field and are
// full-AI snapshots because llm/prompt/tts are required by that branch.
mode = ModeFullAI
}
if mode != ModeFullAI && mode != ModeASROnly {
return Snapshot{}, fmt.Errorf("unsupported AI mode %q", mode)
}
digest := sha256.Sum256(raw)
return Snapshot{AgentVersionID: value.AgentVersionID, Digest: hex.EncodeToString(digest[:]), Raw: append([]byte(nil), raw...), Mode: mode}, nil
}
func ValidateForMode(raw []byte, mode Mode) (Snapshot, error) {
if mode != ModeFullAI && mode != ModeASROnly {
return Snapshot{}, fmt.Errorf("unsupported AI mode %q", mode)
}
snapshot, err := Validate(raw)
if err != nil {
return Snapshot{}, err
}
if snapshot.Mode != mode {
return Snapshot{}, fmt.Errorf("AI config mode %q does not match requested mode %q", snapshot.Mode, mode)
}
return snapshot, nil
}
func EnsureSameVersion(previous, next Snapshot) error {
if previous.AgentVersionID == "" || next.AgentVersionID == "" || previous.AgentVersionID != next.AgentVersionID {
return errors.New("agent version identity changed")
}
if previous.Digest != next.Digest {
return errors.New("immutable agent version content changed")
}
return nil
}
type Cache struct {
mu sync.RWMutex
items map[string]Snapshot
}
func NewCache() *Cache { return &Cache{items: make(map[string]Snapshot)} }
func (c *Cache) Put(tenantKey string, snapshot Snapshot) error {
if err := contract.ValidateTenantKey(tenantKey); err != nil {
return err
}
if snapshot.AgentVersionID == "" || snapshot.Digest == "" {
return errors.New("snapshot identity is required")
}
c.mu.Lock()
defer c.mu.Unlock()
key := tenantKey + "\x00" + snapshot.AgentVersionID
if old, ok := c.items[key]; ok {
if err := EnsureSameVersion(old, snapshot); err != nil {
return err
}
}
c.items[key] = snapshot
return nil
}
func (c *Cache) Get(tenantKey, versionID string) (Snapshot, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
s, ok := c.items[tenantKey+"\x00"+versionID]
return s, ok
}
func ContractSource() string { return contracts.SourceCommit }