Files
tutti/services/tuttid/service/agent/composer_capability_cache.go
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

162 lines
4.3 KiB
Go

package agent
import (
"context"
"strings"
"sync"
"time"
"github.com/tutti-os/tutti/services/tuttid/biz/agentprovider"
)
const (
defaultCapabilityCatalogCacheTTL = 30 * time.Second
defaultCapabilityCatalogErrorCacheTTL = 5 * time.Second
)
type composerCapabilityCatalogCache struct {
mu sync.Mutex
entries map[string]composerCapabilityCatalogCacheEntry
}
type composerCapabilityCatalogCacheEntry struct {
cachedAt time.Time
errors []string
options []ComposerCapabilityOption
}
func newComposerCapabilityCatalogCache() *composerCapabilityCatalogCache {
return &composerCapabilityCatalogCache{
entries: make(map[string]composerCapabilityCatalogCacheEntry),
}
}
func (c *composerCapabilityCatalogCache) get(
key string,
now time.Time,
successTTL time.Duration,
errorTTL time.Duration,
) ([]ComposerCapabilityOption, []string, bool) {
if c == nil {
return nil, nil, false
}
c.mu.Lock()
defer c.mu.Unlock()
entry, ok := c.entries[key]
if !ok {
return nil, nil, false
}
ttl := successTTL
if len(entry.errors) > 0 {
ttl = errorTTL
}
if ttl <= 0 || now.Sub(entry.cachedAt) > ttl {
delete(c.entries, key)
return nil, nil, false
}
return cloneComposerCapabilityOptions(entry.options), append([]string(nil), entry.errors...), true
}
func (c *composerCapabilityCatalogCache) set(key string, now time.Time, options []ComposerCapabilityOption) {
if c == nil {
return
}
c.mu.Lock()
defer c.mu.Unlock()
c.entries[key] = composerCapabilityCatalogCacheEntry{
cachedAt: now,
options: cloneComposerCapabilityOptions(options),
}
}
func (c *composerCapabilityCatalogCache) setFailure(key string, now time.Time, errors []string) {
if c == nil {
return
}
c.mu.Lock()
defer c.mu.Unlock()
c.entries[key] = composerCapabilityCatalogCacheEntry{
cachedAt: now,
errors: append([]string(nil), errors...),
}
}
func (s *Service) listComposerCapabilityOptions(
ctx context.Context,
provider string,
cwd string,
fallbackSkills []ComposerSkillOption,
) ([]ComposerCapabilityOption, []string) {
cacheKey := composerCapabilityCatalogCacheKey(provider, cwd, fallbackSkills)
now := time.Now().UTC()
if cached, errors, ok := s.capabilityCatalogCache.get(
cacheKey,
now,
s.capabilityCatalogCacheTTL(),
defaultCapabilityCatalogErrorCacheTTL,
); ok {
return cached, errors
}
loaded, _, _ := s.capabilityCatalogGroup.Do(cacheKey, func() (any, error) {
if cached, errors, ok := s.capabilityCatalogCache.get(
cacheKey,
time.Now().UTC(),
s.capabilityCatalogCacheTTL(),
defaultCapabilityCatalogErrorCacheTTL,
); ok {
return capabilityCatalogLoadResult{options: cached, errors: errors}, nil
}
options, errors := s.composerCapabilityLister().ListComposerCapabilityOptions(ctx, provider, cwd, fallbackSkills)
if len(errors) == 0 {
s.capabilityCatalogCache.set(cacheKey, time.Now().UTC(), options)
} else {
s.capabilityCatalogCache.setFailure(cacheKey, time.Now().UTC(), errors)
}
return capabilityCatalogLoadResult{options: options, errors: errors}, nil
})
result, ok := loaded.(capabilityCatalogLoadResult)
if !ok {
return nil, []string{"capability catalog load returned an invalid result"}
}
return cloneComposerCapabilityOptions(result.options), append([]string(nil), result.errors...)
}
type capabilityCatalogLoadResult struct {
errors []string
options []ComposerCapabilityOption
}
func (s *Service) capabilityCatalogCacheTTL() time.Duration {
if s.CapabilityCatalogCacheTTL != 0 {
return s.CapabilityCatalogCacheTTL
}
return defaultCapabilityCatalogCacheTTL
}
func composerCapabilityCatalogCacheKey(provider string, cwd string, skills []ComposerSkillOption) string {
var builder strings.Builder
builder.WriteString(agentprovider.Normalize(provider))
builder.WriteByte('\n')
builder.WriteString(strings.TrimSpace(cwd))
for _, skill := range skills {
builder.WriteByte('\n')
builder.WriteString(skill.Name)
builder.WriteByte('|')
builder.WriteString(skill.Trigger)
builder.WriteByte('|')
builder.WriteString(skill.SourceKind)
builder.WriteByte('|')
builder.WriteString(skill.PluginName)
builder.WriteByte('|')
builder.WriteString(skill.Path)
}
return builder.String()
}
func cloneComposerCapabilityOptions(options []ComposerCapabilityOption) []ComposerCapabilityOption {
if len(options) == 0 {
return nil
}
return append([]ComposerCapabilityOption(nil), options...)
}