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
162 lines
4.3 KiB
Go
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...)
|
|
}
|