toolsrv: dynamic tool discovery, drop global cache
Registry.Summaries() and Load() now scan the tools directory on every call instead of reading from a startup cache. New tools dropped into the directory are immediately visible without restarting the server. When tool_list is called, the OnToolsChanged hook fires and updates the preamble's '# Available Tools' section in the live agent runtime. No agent reload or session restart required.
This commit is contained in:
parent
80d7e4436d
commit
10bd60980a
|
|
@ -319,6 +319,35 @@ func (ag *Agent) RenamePreamble(old, new string) {
|
|||
}
|
||||
}
|
||||
|
||||
// refreshToolListing replaces the "# Available Tools" section in the preamble
|
||||
// with an updated listing. Called when tool_list detects directory changes.
|
||||
func (ag *Agent) refreshToolListing(listing string) {
|
||||
if ag.runtime == nil {
|
||||
return
|
||||
}
|
||||
const marker = "\n# Available Tools\n\n"
|
||||
if idx := strings.Index(ag.runtime.Preamble, marker); idx >= 0 {
|
||||
ag.runtime.Preamble = ag.runtime.Preamble[:idx]
|
||||
}
|
||||
if listing != "" {
|
||||
ag.runtime.Preamble += marker + listing
|
||||
}
|
||||
}
|
||||
|
||||
// wireToolsChanged sets the OnToolsChanged callback on the tool server
|
||||
// so the preamble stays in sync when tools are added/removed from disk.
|
||||
func (ag *Agent) wireToolsChanged() {
|
||||
if ag.runtime == nil || ag.runtime.ToolServer == nil {
|
||||
return
|
||||
}
|
||||
type hasHook interface {
|
||||
SetOnToolsChanged(func(string))
|
||||
}
|
||||
if srv, ok := ag.runtime.ToolServer.(hasHook); ok {
|
||||
srv.SetOnToolsChanged(ag.refreshToolListing)
|
||||
}
|
||||
}
|
||||
|
||||
// SaveFull persists the full session state (history + metadata) to the given path.
|
||||
func (ag *Agent) SaveFull(path, sessionID, cwd, remote string) error {
|
||||
if ag.history == nil {
|
||||
|
|
|
|||
|
|
@ -203,6 +203,7 @@ func (ag *Agent) cmdAgent(_ context.Context, args []string) {
|
|||
ag.runtime = rt
|
||||
ag.agentName = name
|
||||
ag.history = nil
|
||||
ag.wireToolsChanged()
|
||||
ag.notifyChange()
|
||||
for _, msg := range rt.Messages {
|
||||
ag.emit(infoEvent(msg))
|
||||
|
|
|
|||
|
|
@ -56,5 +56,6 @@ func NewAgent(cfg AgentCfg) *Agent {
|
|||
}
|
||||
ag.changeCond = sync.NewCond(&ag.changeMu)
|
||||
ag.turnError = ag.defaultTurnError
|
||||
ag.wireToolsChanged()
|
||||
return ag
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,19 @@ func ToolList(ctx context.Context, srv *toolsrv.Server, args json.RawMessage) (s
|
|||
}
|
||||
out.WriteString("\n")
|
||||
}
|
||||
|
||||
// Notify the agent that the tool listing may have changed so the
|
||||
// preamble stays in sync with the directory contents.
|
||||
if srv.OnToolsChanged != nil {
|
||||
var listing strings.Builder
|
||||
for _, s := range summaries {
|
||||
if s.Description != "" {
|
||||
fmt.Fprintf(&listing, "- **%s** — %s\n", s.Name, s.Description)
|
||||
}
|
||||
}
|
||||
srv.OnToolsChanged(listing.String())
|
||||
}
|
||||
|
||||
return strings.TrimRight(out.String(), "\n"), nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,11 @@ func (e *Server) InjectContent(content string) {
|
|||
}
|
||||
}
|
||||
|
||||
// SetOnToolsChanged sets the callback for tool directory changes.
|
||||
func (e *Server) SetOnToolsChanged(fn func(string)) {
|
||||
e.OnToolsChanged = fn
|
||||
}
|
||||
|
||||
// ExecuteInSandbox runs a command in the sandbox. Exported for builtin tools.
|
||||
func (e *Server) ExecuteInSandbox(ctx context.Context, cmd, language string, timeout int, sandbox string, trusted bool, stdin string, detach bool) (string, error) {
|
||||
return e.executeWithStdin(ctx, cmd, language, timeout, sandbox, trusted, stdin, detach)
|
||||
|
|
|
|||
|
|
@ -2,41 +2,40 @@ package toolsrv
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sort"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
|
||||
)
|
||||
|
||||
type Registry struct {
|
||||
mu sync.RWMutex
|
||||
global map[string]ToolInfo
|
||||
sessions map[string]map[string]ToolInfo
|
||||
revisions map[string]uint64
|
||||
}
|
||||
|
||||
func NewRegistry() (*Registry, error) {
|
||||
r := &Registry{
|
||||
global: make(map[string]ToolInfo),
|
||||
sessions: make(map[string]map[string]ToolInfo),
|
||||
revisions: make(map[string]uint64),
|
||||
}
|
||||
if err := r.Discover(); err != nil {
|
||||
return nil, err
|
||||
// Validate that the tools directory is readable.
|
||||
dir := ToolsPath()
|
||||
if _, err := os.ReadDir(dir); err != nil {
|
||||
return nil, fmt.Errorf("read tools dir %s: %w", dir, err)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *Registry) Discover() error {
|
||||
// discover scans the tools directory and returns all valid tool infos.
|
||||
func (r *Registry) discover() map[string]ToolInfo {
|
||||
dir := ToolsPath()
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read tools dir %s: %w", dir, err)
|
||||
return nil
|
||||
}
|
||||
global := make(map[string]ToolInfo)
|
||||
tools := make(map[string]ToolInfo)
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || e.Name() == "idx" || e.Name()[0] == '.' {
|
||||
continue
|
||||
|
|
@ -49,19 +48,15 @@ func (r *Registry) Discover() error {
|
|||
if !strings.Contains(script, "ollie:prompt") {
|
||||
continue
|
||||
}
|
||||
global[e.Name()] = ParseToolInfo(e.Name(), script)
|
||||
tools[e.Name()] = ParseToolInfo(e.Name(), script)
|
||||
}
|
||||
r.mu.Lock()
|
||||
r.global = global
|
||||
r.mu.Unlock()
|
||||
return nil
|
||||
return tools
|
||||
}
|
||||
|
||||
func (r *Registry) Summaries() []ToolInfo {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
tools := r.discover()
|
||||
var summaries []ToolInfo
|
||||
for _, info := range r.global {
|
||||
for _, info := range tools {
|
||||
summaries = append(summaries, ToolInfo{
|
||||
Name: info.Name,
|
||||
Description: info.Description,
|
||||
|
|
@ -72,9 +67,8 @@ func (r *Registry) Summaries() []ToolInfo {
|
|||
}
|
||||
|
||||
func (r *Registry) Load(sessionID, name string) error {
|
||||
r.mu.RLock()
|
||||
tool, exists := r.global[name]
|
||||
r.mu.RUnlock()
|
||||
tools := r.discover()
|
||||
tool, exists := tools[name]
|
||||
if !exists {
|
||||
return fmt.Errorf("tool not found: %s", name)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,10 @@ type Server struct {
|
|||
// should be injected into the agent's context.
|
||||
OnInjection func(content string)
|
||||
|
||||
// OnToolsChanged is called when tool_list detects the available tools
|
||||
// have changed, providing the updated preamble-format listing.
|
||||
OnToolsChanged func(listing string)
|
||||
|
||||
// rate limiting state (per-Server)
|
||||
rateLimitMu sync.Mutex
|
||||
validationFailures int
|
||||
|
|
|
|||
Reference in New Issue