registry: refresh schemas on tool_list and reload on tool_load
tool_load always re-reads the tool from disk (reload semantics). tool_list calls RefreshLoaded to update schemas for all promoted tools. Skills registry gets the same treatment: dynamic discovery, no cache.
This commit is contained in:
parent
6fc2e001ac
commit
a640257dbf
|
|
@ -4,9 +4,9 @@ package skills
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
@ -32,25 +32,33 @@ type SkillInfo struct {
|
||||||
// Tracks which skills are loaded per session.
|
// Tracks which skills are loaded per session.
|
||||||
type Registry struct {
|
type Registry struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
global map[string]SkillInfo
|
|
||||||
sessions map[string]map[string]bool // sessionID → skill name → loaded
|
sessions map[string]map[string]bool // sessionID → skill name → loaded
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRegistry() *Registry {
|
func NewRegistry() *Registry {
|
||||||
return &Registry{
|
return &Registry{
|
||||||
global: make(map[string]SkillInfo),
|
|
||||||
sessions: make(map[string]map[string]bool),
|
sessions: make(map[string]map[string]bool),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Discover scans the skills directory and registers all found skills.
|
// Discover validates that the skills directory is readable.
|
||||||
|
// Retained for backward compatibility; discovery is now dynamic.
|
||||||
func (r *Registry) Discover() error {
|
func (r *Registry) Discover() error {
|
||||||
|
dir := SkillsPath()
|
||||||
|
if _, err := os.ReadDir(dir); err != nil {
|
||||||
|
return fmt.Errorf("read skills dir %s: %w", dir, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// discover scans the skills directory and returns all found skills.
|
||||||
|
func (r *Registry) discover() map[string]SkillInfo {
|
||||||
dir := SkillsPath()
|
dir := SkillsPath()
|
||||||
entries, err := os.ReadDir(dir)
|
entries, err := os.ReadDir(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("read skills dir %s: %w", dir, err)
|
return nil
|
||||||
}
|
}
|
||||||
global := make(map[string]SkillInfo)
|
skills := make(map[string]SkillInfo)
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
if !e.IsDir() {
|
if !e.IsDir() {
|
||||||
continue
|
continue
|
||||||
|
|
@ -67,24 +75,20 @@ func (r *Registry) Discover() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
global[name] = SkillInfo{
|
skills[name] = SkillInfo{
|
||||||
Name: meta.Name,
|
Name: meta.Name,
|
||||||
Description: meta.Description,
|
Description: meta.Description,
|
||||||
Dir: skillDir,
|
Dir: skillDir,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
r.mu.Lock()
|
return skills
|
||||||
r.global = global
|
|
||||||
r.mu.Unlock()
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Summaries returns all discovered skills.
|
// Summaries returns all discovered skills.
|
||||||
func (r *Registry) Summaries() []SkillInfo {
|
func (r *Registry) Summaries() []SkillInfo {
|
||||||
r.mu.RLock()
|
skills := r.discover()
|
||||||
defer r.mu.RUnlock()
|
out := make([]SkillInfo, 0, len(skills))
|
||||||
out := make([]SkillInfo, 0, len(r.global))
|
for _, info := range skills {
|
||||||
for _, info := range r.global {
|
|
||||||
out = append(out, info)
|
out = append(out, info)
|
||||||
}
|
}
|
||||||
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||||
|
|
@ -93,10 +97,8 @@ func (r *Registry) Summaries() []SkillInfo {
|
||||||
|
|
||||||
// Load marks a skill as loaded for the given session.
|
// Load marks a skill as loaded for the given session.
|
||||||
func (r *Registry) Load(sessionID, name string) error {
|
func (r *Registry) Load(sessionID, name string) error {
|
||||||
r.mu.RLock()
|
skills := r.discover()
|
||||||
_, exists := r.global[name]
|
if _, exists := skills[name]; !exists {
|
||||||
r.mu.RUnlock()
|
|
||||||
if !exists {
|
|
||||||
return fmt.Errorf("skill not found: %s", name)
|
return fmt.Errorf("skill not found: %s", name)
|
||||||
}
|
}
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
|
|
@ -124,14 +126,15 @@ func (r *Registry) Unload(sessionID, name string) error {
|
||||||
// Loaded returns skills currently loaded for a session.
|
// Loaded returns skills currently loaded for a session.
|
||||||
func (r *Registry) Loaded(sessionID string) []SkillInfo {
|
func (r *Registry) Loaded(sessionID string) []SkillInfo {
|
||||||
r.mu.RLock()
|
r.mu.RLock()
|
||||||
defer r.mu.RUnlock()
|
|
||||||
names, ok := r.sessions[sessionID]
|
names, ok := r.sessions[sessionID]
|
||||||
|
r.mu.RUnlock()
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
skills := r.discover()
|
||||||
out := make([]SkillInfo, 0, len(names))
|
out := make([]SkillInfo, 0, len(names))
|
||||||
for name := range names {
|
for name := range names {
|
||||||
if info, exists := r.global[name]; exists {
|
if info, exists := skills[name]; exists {
|
||||||
out = append(out, info)
|
out = append(out, info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -142,9 +145,8 @@ func (r *Registry) Loaded(sessionID string) []SkillInfo {
|
||||||
// SkillContent reads and returns the full content of a skill's SKILL.md file,
|
// SkillContent reads and returns the full content of a skill's SKILL.md file,
|
||||||
// with front-matter stripped. Returns (content, error).
|
// with front-matter stripped. Returns (content, error).
|
||||||
func (r *Registry) SkillContent(name string) (string, error) {
|
func (r *Registry) SkillContent(name string) (string, error) {
|
||||||
r.mu.RLock()
|
skills := r.discover()
|
||||||
info, exists := r.global[name]
|
info, exists := skills[name]
|
||||||
r.mu.RUnlock()
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return "", fmt.Errorf("skill not found: %s", name)
|
return "", fmt.Errorf("skill not found: %s", name)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,13 @@ func ToolList(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (s
|
||||||
if reg == nil {
|
if reg == nil {
|
||||||
return "", fmt.Errorf("tool_list: no registry available")
|
return "", fmt.Errorf("tool_list: no registry available")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refresh loaded tool schemas from disk so the model sees current definitions.
|
||||||
|
sid := srv.SessionID()
|
||||||
|
if sid != "" {
|
||||||
|
reg.RefreshLoaded(sid)
|
||||||
|
}
|
||||||
|
|
||||||
summaries := reg.Summaries()
|
summaries := reg.Summaries()
|
||||||
if len(summaries) == 0 {
|
if len(summaries) == 0 {
|
||||||
return "(no tools found)", nil
|
return "(no tools found)", nil
|
||||||
|
|
|
||||||
|
|
@ -80,10 +80,6 @@ func (r *Registry) Load(sessionID, name string) error {
|
||||||
r.sessions[sessionID] = make(map[string]ToolInfo)
|
r.sessions[sessionID] = make(map[string]ToolInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, already := r.sessions[sessionID][name]; already {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
r.sessions[sessionID][name] = tool
|
r.sessions[sessionID][name] = tool
|
||||||
r.revisions[sessionID]++
|
r.revisions[sessionID]++
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -124,6 +120,41 @@ func (r *Registry) Loaded(sessionID string) []ToolInfo {
|
||||||
return loaded
|
return loaded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RefreshLoaded re-reads all loaded tools from disk, updating their schemas
|
||||||
|
// and metadata in place. Called from tool_list to keep schemas current.
|
||||||
|
func (r *Registry) RefreshLoaded(sessionID string) {
|
||||||
|
r.mu.RLock()
|
||||||
|
sessionTools, ok := r.sessions[sessionID]
|
||||||
|
if !ok || len(sessionTools) == 0 {
|
||||||
|
r.mu.RUnlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
names := make([]string, 0, len(sessionTools))
|
||||||
|
for name := range sessionTools {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
r.mu.RUnlock()
|
||||||
|
|
||||||
|
fresh := r.discover()
|
||||||
|
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
changed := false
|
||||||
|
for _, name := range names {
|
||||||
|
if tool, exists := fresh[name]; exists {
|
||||||
|
r.sessions[sessionID][name] = tool
|
||||||
|
changed = true
|
||||||
|
} else {
|
||||||
|
// Tool was removed from disk — unload it.
|
||||||
|
delete(r.sessions[sessionID], name)
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
r.revisions[sessionID]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Registry) Lookup(sessionID, name string) (ToolInfo, bool) {
|
func (r *Registry) Lookup(sessionID, name string) (ToolInfo, bool) {
|
||||||
r.mu.RLock()
|
r.mu.RLock()
|
||||||
defer r.mu.RUnlock()
|
defer r.mu.RUnlock()
|
||||||
|
|
|
||||||
Reference in New Issue